Você pode publicar em um broker MQTT a partir do AWS Lambda (ou de qualquer função serverless) com uma biblioteca cliente MQTT comum — o problema é o ciclo de vida. Uma função vive algumas centenas de milissegundos, é congelada entre invocações e pode rodar em dezenas de cópias paralelas. Conecte sob demanda, reaproveite o cliente entre invocações a quente, espere a confirmação do publish antes de retornar e dê a cada ambiente de execução seu próprio client ID. Se você só publica, uma ponte HTTP pode ser ainda mais simples.
Este tutorial mostra um handler Python pronto para produção (paho-mqtt 2.x), a mesma ideia em Node.js, a alternativa HTTP e como testar tudo.
O ciclo de vida serverless versus MQTT
O MQTT pressupõe conexões TCP de longa duração com pings de keep-alive. O Lambda pressupõe execuções curtas e sem estado. O descompasso aparece em quatro pontos:
| Comportamento do Lambda | Efeito em um cliente MQTT |
|---|---|
| Cold start (novo ambiente de execução) | Handshake completo TCP + TLS + CONNECT antes do primeiro publish |
| Invocação a quente (ambiente reutilizado) | As variáveis globais sobrevivem, então uma conexão em cache pode ser reaproveitada |
| Congelamento entre invocações | Threads em segundo plano param; nenhum ping de keep-alive é enviado |
| Ambientes paralelos sob carga | Muitos clientes simultâneos, cada um precisando do próprio client ID |
O congelamento é o ponto sutil. Se um ambiente ficar ocioso por mais de cerca de 1,5× o intervalo de keep-alive, o broker fecha a conexão. Quando o ambiente descongela, o cliente ainda acha que está conectado até tentar escrever. Um cliente em cache ingênuo vai falhar ou travar na primeira invocação depois de um período parado.
Estratégia 1: conectar a cada invocação
O padrão correto mais simples: conectar, publicar, esperar o PUBACK, desconectar. Nada sobrevive entre chamadas, então nada fica obsoleto.
- Prós: nenhum bug de conexão obsoleta, nenhuma colisão de client ID se você usar um ID aleatório, fácil de entender.
- Contras: cada invocação paga o handshake de conexão — bem mais com TLS e a partir de uma região distante — e uma rajada de invocações vira uma rajada de novas conexões no broker.
É uma boa escolha para gatilhos de baixo volume: um job noturno, uma notificação de upload no S3, um webhook ocasional.
Estratégia 2: reaproveitar a conexão entre invocações a quente
Para qualquer coisa frequente, crie o cliente no escopo global para que as invocações a quente pulem o handshake, e desconfie da conexão em cache depois de um período ocioso.
import json, os, threading, time, uuid
import paho.mqtt.client as mqtt
HOST = os.environ.get("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.environ.get("MQTT_PORT", "8883"))
KEEPALIVE = 60
MAX_IDLE = KEEPALIVE # depois desse intervalo, assuma que o broker nos derrubou
_client = None
_connected = threading.Event()
_last_used = 0.0
def _on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"connect refused: {reason_code}")
else:
_connected.set()
def _on_disconnect(client, userdata, flags, reason_code, properties):
_connected.clear()
def _new_client():
c = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=f"lambda-{uuid.uuid4().hex[:12]}", # único por ambiente
protocol=mqtt.MQTTv5,
)
c.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
c.tls_set() # bundle de CAs do sistema
c.on_connect, c.on_disconnect = _on_connect, _on_disconnect
c.connect_async(HOST, PORT, keepalive=KEEPALIVE)
c.loop_start()
return c
def _get_client(timeout=5.0):
global _client
if _client is not None and time.time() - _last_used > MAX_IDLE:
_client.disconnect() # obsoleta após um congelamento
_client.loop_stop()
_connected.clear()
_client = None
if _client is None:
_client = _new_client()
if not _connected.wait(timeout):
raise TimeoutError("MQTT connect timed out")
return _client
def handler(event, context):
global _last_used
client = _get_client()
payload = json.dumps({"source": "lambda", "event": event, "ts": time.time()})
info = client.publish("testmqtt/lambda/events", payload, qos=1)
info.wait_for_publish(timeout=5)
if not info.is_published():
raise RuntimeError("PUBACK not received") # deixe o Lambda tentar de novo
_last_used = time.time()
return {"statusCode": 200, "body": json.dumps({"mid": info.mid})}O que cada parte faz:
connect_async+loop_start— o loop de rede roda em uma thread em segundo plano, que cuida deCONNACK,PUBACKe reconexões enquanto a função está rodando.MAX_IDLE— se o ambiente ficou ocioso mais que o keep-alive, o código descarta o cliente e reconecta em vez de escrever em um socket morto.wait_for_publish— nunca retorne antes de chegar a confirmação do QoS 1. Depois que o handler retorna, o ambiente pode ser congelado com a mensagem ainda no buffer do cliente.- Lançar erro em caso de falha — em gatilhos assíncronos, o Lambda tenta de novo as invocações que falharam, que é exatamente o que você quer para um publish perdido.
- Client ID aleatório — dois ambientes concorrentes com o mesmo ID ficariam se derrubando (client ID duplicado).
Empacote com pip install paho-mqtt -t package/ e compacte a pasta junto com o handler, ou use uma Lambda layer. Defina o timeout da função bem acima do timeout de conexão (10–15 s é um bom ponto de partida) e guarde as credenciais em variáveis de ambiente alimentadas pelo Secrets Manager ou pelo Parameter Store.
O mesmo padrão em Node.js
Com o MQTT.js 5, o cliente em cache é uma variável no nível do módulo. A biblioteca reconecta sozinha, mas depois de um congelamento ainda vale verificar a conexão antes de publicar.
import mqtt from 'mqtt';
import { randomUUID } from 'node:crypto';
let client;
let lastUsed = 0;
async function getClient() {
if (client && Date.now() - lastUsed > 60_000) { await client.endAsync(true); client = undefined; }
if (!client) {
client = await mqtt.connectAsync('mqtts://broker.connectmq.com:8883', {
clientId: `lambda-${randomUUID().slice(0, 12)}`,
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
keepalive: 60,
connectTimeout: 5000,
});
}
return client;
}
export const handler = async (event) => {
const c = await getClient();
await c.publishAsync('testmqtt/lambda/events', JSON.stringify({ source: 'lambda', event }), { qos: 1 });
lastUsed = Date.now();
return { statusCode: 200 };
};publishAsync com QoS 1 só resolve depois do PUBACK, então retornar após o await é seguro.
Estratégia 3: publicar via HTTP
Se suas funções só publicam e não precisam de semântica MQTT no cliente, uma requisição HTTP combina naturalmente com serverless: sem estado de conexão, sem client IDs, sem problemas de congelamento.
- APIs HTTP do broker. Alguns brokers expõem um endpoint REST que publica uma mensagem por você — por exemplo a API HTTP do EMQX e o endpoint HTTPS do data plane do AWS IoT Core. Veja a documentação do seu broker para autenticação e formato de payload.
- Sua própria ponte. Um pequeno serviço sempre ligado que mantém uma conexão MQTT e aceita requisições
POST /publishautenticadas. Ele centraliza as credenciais e transforma mil conexões efêmeras de Lambda em uma única persistente.
A contrapartida: você perde a QoS de ponta a ponta — o HTTP 200 diz que a ponte aceitou a mensagem, não que um subscriber a recebeu — e ganha mais um componente para operar. Para uma comparação mais ampla dos dois protocolos, veja MQTT vs HTTP.
E para assinar?
Uma função Lambda não pode ser um subscriber MQTT de longa duração: ela só roda enquanto trata uma invocação. Inverta a direção — deixe algo sempre ligado assinar e invocar a função:
- Uma regra ou integração do broker que encaminha as mensagens correspondentes para um endpoint HTTP (API Gateway ou uma function URL do Lambda). Veja MQTT para webhook para o padrão geral.
- Um pequeno serviço assinante que envia as mensagens para o SQS, que por sua vez dispara o Lambda em lotes.
Erros comuns
Task timed out after 3.00 seconds— o timeout padrão do Lambda é menor que uma conexão TLS a frio a partir de outra região. Aumente-o e confira se a porta do broker está acessível.- O connect trava dentro de uma VPC — funções em VPC precisam de um NAT gateway para sair para a internet. Veja timeout de conexão MQTT.
Not authorized/ reason code0x87— credenciais erradas ou uma ACL que não libera o topic; a referência de reason codes traz o significado exato.- Mensagens somem depois que a função retornou — você retornou antes do
PUBACK. Sempre espere o publish terminar.
Testando
- Abra o cliente MQTT online, conecte em
broker.connectmq.come assinetestmqtt/lambda/#. - Invoque a função pelo console ou com
aws lambda invoke --function-name mqtt-publisher --payload '{"hello":"world"}' --cli-binary-format raw-in-base64-out out.json. - A mensagem deve aparecer no navegador em menos de um segundo. Invoque de novo logo em seguida: a invocação a quente deve ser visivelmente mais rápida na métrica de duração da função.
- Espere alguns minutos, invoque outra vez e confirme que o caminho de conexão obsoleta reconecta de forma limpa em vez de dar timeout.
Veja as mensagens do seu Lambda chegando
O endpoint de teste compartilhado tem limite de taxa e não guarda sessões, o que basta para essa verificação; para testes de carga com muitas invocações paralelas, aponte a função para a sua própria instância privada de broker.
Serverless e MQTT funcionam bem juntos quando você respeita o ciclo de vida: conecte sob demanda, desconfie de conexões depois de um congelamento, espere a confirmação e dê a cada ambiente sua própria identidade. Quando você só precisa publicar em escala sem esperar resposta, uma ponte HTTP elimina totalmente o gerenciamento de conexões.
Perguntas frequentes
Uma função AWS Lambda pode assinar topics MQTT?
Não de forma útil. A função só roda enquanto trata uma invocação e fica congelada entre elas, então não consegue manter uma assinatura. Deixe um serviço de longa duração ou uma integração do broker assinar e invocar a função por HTTP ou por uma fila.
Devo abrir uma nova conexão MQTT a cada invocação do Lambda?
É a abordagem correta mais simples, mas cada chamada paga o handshake TCP e TLS. Guardar o cliente no escopo global elimina esse custo nas invocações a quente, desde que você detecte conexões que morreram enquanto o ambiente estava congelado.
Por que meu publish no Lambda dá timeout dentro de uma VPC?
Funções ligadas a uma VPC não têm acesso à internet a menos que a sub-rede saia por um NAT gateway (ou o broker seja acessível de forma privada). Sem isso, o connect TCP ao broker trava até a função estourar o tempo.



