Uma queda de broker MQTT não é uma questão de se, mas de quando: redes se particionam, certificados são renovados, brokers recebem upgrades e regiões de nuvem têm dias ruins. Este guia mostra como projetar clients de dispositivos e de backend para que uma queda do broker, curta ou longa, não perca nenhum dado importante nem cause ações duplicadas. Os padrões valem para qualquer broker, incluindo a ConnectMQ, e vêm com código Python funcional.
O Que Realmente Acontece Durante uma Queda de Broker MQTT
Do ponto de vista do client, uma queda se parece com uma de três coisas: a conexão TCP cai, a conexão trava silenciosamente até o keepalive expirar, ou novas tentativas de conexão são recusadas. A causa pode ser o próprio broker, a rede entre você e o broker, DNS, TLS ou até as suas próprias credenciais. O client nem sempre consegue saber qual delas, então precisa tratar todas da mesma forma: perceber rápido, continuar trabalhando localmente e reconectar com educação.
Quatro coisas ficam em risco:
- Dados de saída publicados enquanto desconectado se perdem, a menos que você os armazene em buffer.
- Comandos de entrada enviados enquanto você estava offline podem ou não ser entregues, dependendo das configurações de sessão.
- Informações de presença ficam erradas se outros sistemas ainda acharem que o dispositivo está online.
- O próprio broker pode ser sobrecarregado quando milhares de dispositivos reconectam no mesmo instante.
Um bom design de client trata cada uma delas. Vale ler o que um broker MQTT faz com sessões e mensagens enfileiradas se esses termos forem novos para você.
Padrão 1: Reconectar com Backoff Exponencial e Jitter
A abordagem ingênua é tentar de novo a cada segundo, para sempre. Multiplique isso por dez mil dispositivos e, no momento em que o broker volta, ele recebe uma muralha de conexões simultâneas, que pode derrubá-lo de novo na hora. Esse é o problema do thundering herd (efeito manada).
A solução é backoff exponencial com jitter aleatório: espere 1 s, depois 2 s, 4 s, 8 s e assim por diante até um teto, e randomize cada atraso para que os dispositivos se espalhem. O Paho 2.x tem suporte nativo via reconnect_delay_set, mas não adiciona jitter, por isso muitas equipes controlam o loop por conta própria para ter controle total.
Cuide também das suas credenciais. Brokers que bloqueiam endereços IP após repetidas falhas de login, como a ConnectMQ faz, vão tratar um dispositivo com senha errada tentando a cada segundo como um ataque. Aplique backoff em erros de autenticação de forma bem mais agressiva do que em erros de rede e leve-os ao conhecimento de um operador.
Padrão 2: Armazenar os Dados de Saída Localmente
As bibliotecas de client MQTT mantêm as mensagens em trânsito na memória, que desaparece se o dispositivo reiniciar durante a queda. Para telemetria que você não pode perder, grave cada leitura primeiro em um pequeno armazenamento local e apague-a somente depois que o broker confirmá-la com um PUBACK. O SQLite é uma boa escolha em gateways e dispositivos Linux; em microcontroladores, um buffer circular na flash cumpre o mesmo papel.
Decida de antemão o que acontece quando o buffer enche. Para a maior parte da telemetria, descartar as leituras mais antigas é o correto. Para eventos de faturamento ou de segurança, talvez você prefira parar de amostrar dados não críticos.
Padrão 3: Anunciar Presença com Last Will e Status Retido
Defina um Last Will and Testament (LWT) ao conectar: uma mensagem offline retida que o broker publica em seu nome se você sumir sem uma desconexão limpa. A cada conexão bem-sucedida, publique um online retido no mesmo topic. Assim, outros sistemas veem a presença correta sem precisar fazer polling.
Seja preciso quanto à semântica do LWT. O will é publicado quando o broker detecta que a sessão terminou de forma anormal, normalmente após 1,5 vez o intervalo de keepalive. Se o próprio broker estiver fora do ar, nada é publicado até ele voltar, e após um restart do broker o estado retido pode precisar ser restabelecido pelos clients. O MQTT 5 adiciona um will delay interval, que permite suprimir mensagens offline intermitentes durante uma reconexão breve.
Padrão 4: Tornar os Comandos Idempotentes
Com QoS 1, uma mensagem é entregue pelo menos uma vez. Durante uma queda e reconexão, duplicatas se tornam prováveis. Um comando "alternar relé" entregue duas vezes desliga o relé de novo; "dispensar 10 ml" entregue duas vezes dispensa 20 ml. O remédio é tornar todo comando idempotente:
- Envie estados absolutos (
{"relay": "on"}) em vez de ações relativas (toggle). - Inclua um
command_idúnico em cada comando e faça o dispositivo lembrar os IDs executados recentemente. - Inclua uma expiração: um message expiry interval do MQTT 5 ou um timestamp
valid_until, para que um comando enfileirado durante uma queda longa não seja executado horas depois, quando já não é seguro.
Um Client Python Resiliente
O client a seguir combina os quatro padrões. Ele armazena a telemetria em SQLite, reconecta com backoff exponencial randomizado, mantém um topic de status retido com LWT e elimina comandos duplicados. Ele usa o servidor MQTT público, feito para testes, então você pode executá-lo na hora; em produção, mude para a porta 8883, TLS e as credenciais do portal.
import json
import random
import sqlite3
import time
import uuid
import paho.mqtt.client as mqtt
HOST, PORT = "broker.connectmq.com", 1883
DEVICE = f"dev-{uuid.uuid4().hex[:6]}"
ROOT = f"connectmq/demo/{DEVICE}"
STATUS, TELEMETRY, COMMANDS = f"{ROOT}/status", f"{ROOT}/telemetry", f"{ROOT}/cmd"
db = sqlite3.connect("outbox.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS outbox (id INTEGER PRIMARY KEY, payload TEXT)")
db.commit()
pending = {} # mid -> id da linha no outbox
seen_commands = [] # IDs de comandos recentes para eliminar duplicatas
connected = False
def on_connect(client, userdata, flags, reason_code, properties):
global connected
if reason_code.is_failure:
print("Connect refused:", reason_code)
return
connected = True
print("Connected")
client.publish(STATUS, "online", qos=1, retain=True)
client.subscribe(COMMANDS, qos=1)
def on_disconnect(client, userdata, flags, reason_code, properties):
global connected
connected = False
pending.clear() # linhas não confirmadas ficam no outbox e são reenviadas
print("Disconnected:", reason_code)
def on_publish(client, userdata, mid, reason_code, properties):
row_id = pending.pop(mid, None)
if row_id is not None:
db.execute("DELETE FROM outbox WHERE id = ?", (row_id,))
db.commit()
def on_message(client, userdata, msg):
try:
cmd = json.loads(msg.payload)
except ValueError:
print("Ignoring malformed command")
return
if cmd.get("command_id") in seen_commands:
print("Duplicate command ignored:", cmd["command_id"])
return
if cmd.get("valid_until", float("inf")) < time.time():
print("Expired command ignored:", cmd.get("command_id"))
return
seen_commands.append(cmd.get("command_id"))
del seen_commands[:-100]
print("Executing:", cmd)
def flush_outbox(client):
rows = db.execute("SELECT id, payload FROM outbox ORDER BY id LIMIT 20").fetchall()
for row_id, payload in rows:
if row_id in pending.values():
continue
info = client.publish(TELEMETRY, payload, qos=1)
pending[info.mid] = row_id
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=DEVICE)
client.will_set(STATUS, "offline", qos=1, retain=True)
client.on_connect, client.on_disconnect = on_connect, on_disconnect
client.on_publish, client.on_message = on_publish, on_message
# Backoff exponencial entre reconexões; um mínimo aleatório espalha os dispositivos
client.reconnect_delay_set(min_delay=random.randint(1, 5), max_delay=120)
client.connect_async(HOST, PORT, keepalive=30) # o loop tenta de novo se falhar
client.loop_start()
while True:
reading = {"temperature": round(random.uniform(20, 25), 2), "ts": int(time.time())}
db.execute("INSERT INTO outbox (payload) VALUES (?)", (json.dumps(reading),))
db.commit()
if connected:
flush_outbox(client)
time.sleep(5)
Teste desconectando sua rede por um minuto. As leituras se acumulam em outbox.db, o loop de rede continua tentando com atrasos crescentes e, quando a rede volta, o backlog é escoado em ordem, até vinte linhas por ciclo. O Paho dobra o atraso entre tentativas até max_delay, mas não adiciona jitter próprio, e é por isso que cada dispositivo escolhe um atraso inicial aleatório. Em uma frota grande, você pode ir além e adicionar jitter aleatório a cada tentativa com seu próprio loop de reconexão.
Estado de Sessão e o Que Não Presumir
Sessões persistentes (clean session false no MQTT 3.1.1 ou um session expiry interval no MQTT 5) permitem que o broker enfileire mensagens QoS 1 e 2 para um client desconectado. Elas ajudam durante quedas do lado do client. Durante quedas do lado do broker, só ajudam se o broker persistir as sessões entre restarts, o que nem todo broker faz. A ConnectMQ não garante persistência entre restarts, e no servidor MQTT público as sessões expiram em no máximo 30 minutos.
Portanto, projete de forma defensiva:
- Refaça as assinaturas em
on_connecttoda vez, em vez de confiar que o broker lembre delas. - Publique novamente o status e a configuração retidos após reconectar.
- Trate o buffer no dispositivo, e não as filas do broker, como a fonte da verdade para dados que você não pode perder.
- Deixe o backend reconciliar o estado após um intervalo, por exemplo pedindo aos dispositivos que publiquem de novo a configuração atual.
Checklist de Preparação para Quedas
| Área | Pergunta | Boa resposta |
|---|---|---|
| Reconexão | Todo client usa backoff com jitter? | Sim, com teto de um a dois minutos |
| Erros de autenticação | Credenciais erradas são retentadas devagar e geram alerta? | Sim, separadas dos erros de rede |
| Buffer | Onde ficam os dados não enviados durante uma queda? | Armazenamento local persistente com limite de tamanho |
| Presença | Como os outros descobrem que um dispositivo sumiu? | LWT mais status retido |
| Comandos | O que acontece se um comando chegar duas vezes? | Nada; os comandos são idempotentes |
| Comandos antigos | Um comando velho pode rodar após uma queda longa? | Não; a expiração é aplicada |
| Assinaturas | As assinaturas são restauradas após a reconexão? | Sim, em on_connect |
| Testes | Você já ensaiou uma queda? | Sim, cortando a rede em um teste |
Para medidas no lado do broker que complementam esses padrões de client, veja nossos guias sobre alta disponibilidade de broker MQTT e monitoramento de broker MQTT, e passe pelo checklist de produção MQTT antes do lançamento.
Rode Clients Resilientes na ConnectMQ
Um broker MQTT gerenciado elimina as quedas autoinfligidas mais comuns, como certificados expirados e servidores sem patches, enquanto seus clients cuidam do resto. Crie um broker gratuito em app.connectmq.com no plano Serverless, sem cartão de crédito, teste esses padrões contra ele e use o estado de conexão $SYS e o log de alertas do portal para acompanhar as reconexões. Leia mais sobre a ConnectMQ ou compare opções no nosso guia de broker MQTT na nuvem.
Perguntas frequentes
Com que rapidez um client IoT deve reconectar após uma queda de broker MQTT?
Tente rápido no início e depois aplique backoff exponencial até um teto de um a dois minutos, com jitter aleatório em cada atraso. Isso recupera rápido de pequenas falhas sem sobrecarregar o broker após uma queda longa.
O QoS 1 protege os dados durante uma queda do broker?
Apenas para mensagens já entregues à biblioteca do client, e só enquanto o dispositivo permanece ligado. Para dados que você não pode perder, armazene-os em um armazenamento local persistente até chegar um PUBACK.
Quando a mensagem Last Will é enviada?
Quando o broker detecta uma desconexão anormal, normalmente após cerca de 1,5 vez o intervalo de keepalive. Ela não é enviada em uma desconexão limpa, e nada é enviado enquanto o próprio broker está fora do ar.
Os comandos devem usar QoS 2 para evitar duplicatas?
O QoS 2 evita duplicatas entre client e broker, mas duplicatas ponta a ponta ainda podem ocorrer após reconexões ou novas tentativas da aplicação. Comandos idempotentes com IDs únicos são a solução mais robusta.



