Mensagens duplicadas MQTT não são um bug do seu broker; são uma consequência documentada do QoS 1, que promete entrega "pelo menos uma vez" (at least once). Quando uma falha momentânea de rede engole uma confirmação, o remetente retransmite e seu consumidor vê a mesma mensagem duas vezes. Neste guia de solução de problemas você vai entender exatamente quando as duplicatas acontecem, o que a flag DUP diz e o que ela não diz, e como escrever um consumidor idempotente em Python que as trate corretamente.
Por que o QoS 1 produz duplicatas
O QoS 1 usa um handshake de dois passos em cada salto: o remetente transmite o PUBLISH e o receptor responde com PUBACK. Até o PUBACK chegar, o remetente guarda a mensagem e vai reenviá-la. O artigo níveis de QoS do MQTT explicados cobre os três níveis; aqui focamos no que dá errado com o nível 1.
Uma duplicata é criada sempre que o receptor recebeu a mensagem, mas o remetente não recebeu a confirmação:
- O dispositivo publica a leitura #42 no broker.
- O broker a recebe, a encaminha aos assinantes e envia o PUBACK.
- A conexão cai antes de o PUBACK chegar ao dispositivo.
- O dispositivo se reconecta com sua sessão e reenvia a #42 com a flag DUP ativada.
- O broker a trata como uma nova mensagem e a encaminha de novo.
O mesmo acontece no salto do broker para o assinante. Se o assinante processou uma mensagem, mas seu PUBACK se perdeu, o broker a reenvia quando a sessão do assinante é retomada.
Note que uma duplicata pode surgir em qualquer um dos saltos de forma independente, então uma única mensagem publicada pode, ocasionalmente, chegar a um consumidor mais de duas vezes.
O que a flag DUP informa
Todo pacote PUBLISH tem um bit DUP. O remetente o ativa quando está retransmitindo um pacote QoS 1 ou QoS 2. É tentador descartar toda mensagem com DUP ativado, mas isso está errado por vários motivos:
| Suposição | Realidade |
|---|---|
| DUP significa que eu já tenho esta mensagem | Não necessariamente. A primeira cópia pode ter se perdido, e a retentativa pode ser a única cópia que você recebe |
| Sem DUP significa que é nova | Não. O broker encaminha a retentativa de um publicador como uma entrega nova, sem DUP no salto de saída |
| Packet identifiers detectam duplicatas | Só dentro de uma sessão e de uma janela de mensagens em trânsito. Os IDs são reutilizados o tempo todo |
| O QoS 2 resolve tudo | O QoS 2 evita duplicatas por salto, mas retentativas no nível da aplicação ainda podem criá-las |
A flag DUP é útil para logs e diagnóstico. Ela não é uma chave de deduplicação. A deduplicação de verdade precisa de um identificador que você mesmo coloca na mensagem.
Reproduzindo duplicatas
A reentrega real depende de pacotes perdidos e é difícil de provocar sob demanda. A forma confiável de testar seu consumidor é injetar as duplicatas você mesmo. No servidor MQTT público em broker.connectmq.com, pensado para testes e protótipos e não para dados de produção, publique o mesmo ID de mensagem duas vezes:
for n in 1 2; do
mosquitto_pub -h broker.connectmq.com -p 1883 -i dup-pub-t9v4 -q 1 \
-t connectmq/demo/t9v4/meter-3/energy \
-m '{"msgId":"meter-3-000042","kwh":0.25}'
done
Um consumidor que soma kwh sem deduplicação agora informa 0.5 em vez de 0.25. É exatamente o tipo de bug que a reentrega do QoS 1 causa em produção, por exemplo na medição de energia de uma planta industrial, só que mais raro e mais difícil de perceber.
Construindo um consumidor idempotente
Um consumidor idempotente produz o mesmo resultado quer processe uma mensagem uma vez, quer cinco. Há duas técnicas comuns:
- Idempotência natural. Desenhe o efeito para que a repetição seja inofensiva: "defina a temperatura como 21.5" em vez de "aumente em 0.5"; faça upsert de uma linha com chave por dispositivo e timestamp em vez de inserir.
- Deduplicação explícita. Dê a cada mensagem um ID único e lembre-se dos IDs processados recentemente.
Aqui está um consumidor completo com paho-mqtt 2.x que usa deduplicação explícita com um cache limitado em tamanho e tempo:
import json
import time
import uuid
from collections import OrderedDict
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
TOPIC = "connectmq/demo/t9v4/+/energy"
DEDUP_TTL = 600 # segundos para lembrar um ID
DEDUP_MAX = 50_000 # limita o uso de memória
seen = OrderedDict() # msgId -> momento em que foi visto pela primeira vez
totals = {}
def already_seen(msg_id):
now = time.time()
while seen:
oldest_id, ts = next(iter(seen.items()))
if now - ts > DEDUP_TTL or len(seen) > DEDUP_MAX:
seen.popitem(last=False)
else:
break
if msg_id in seen:
return True
seen[msg_id] = now
return False
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(TOPIC, qos=1)
def on_message(client, userdata, msg):
try:
data = json.loads(msg.payload)
msg_id = data["msgId"]
except (ValueError, KeyError):
print("rejected message without msgId")
return
if already_seen(msg_id):
print(f"duplicate {msg_id} (dup flag={msg.dup}) ignored")
return
meter = msg.topic.split("/")[-2]
totals[meter] = totals.get(meter, 0) + data["kwh"]
print(f"{meter} total={totals[meter]:.2f} kWh")
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=f"dedup-{uuid.uuid4().hex[:8]}")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, keepalive=30)
client.loop_forever()
Execute-o e depois rode o loop de publicação dupla da seção anterior. A primeira mensagem atualiza o total; a segunda é reportada como duplicata e ignorada.
Escolhendo um bom ID de mensagem
O ID deve ser criado pelo publicador original, uma única vez, e reutilizado em cada retentativa. Opções:
- ID do dispositivo mais número de sequência, como
meter-3-000042. Compacto e também útil para verificações de ordem de mensagens MQTT. Persista o contador ou adicione um boot ID para que ele não se repita após um reboot. - UUID gerado por mensagem. Simples e globalmente único, ao custo de 36 bytes.
- ID do dispositivo mais timestamp da medição, se o dispositivo nunca produzir duas leituras com o mesmo timestamp.
Não gere o ID no consumidor nem em uma camada intermediária; nesse ponto uma duplicata já parece uma mensagem nova.
Escalando a deduplicação
O cache em memória acima funciona para um único processo consumidor. Para sistemas maiores:
- Restrições no banco de dados. Um índice único em
msg_idtransforma duplicatas em conflitos inofensivos. Com um upsert (INSERT ... ON CONFLICT DO NOTHINGno PostgreSQL), muitas vezes essa é a única deduplicação de que você precisa. - Cache compartilhado. Se vários consumidores em uma shared subscription podem receber retentativas da mesma mensagem, o conjunto de IDs vistos precisa ser compartilhado, por exemplo no Redis com TTL.
- Dimensionamento do TTL. A janela só precisa cobrir atrasos realistas de reentrega: o session expiry mais uma margem. Duplicatas raramente chegam com horas de diferença, a menos que as sessões durem tanto.
Basta usar QoS 2?
O QoS 2 adiciona um handshake de quatro passos que garante entrega exactly-once em cada salto. Ele custa mais idas e voltas e mais estado no broker, e não protege você de um dispositivo que estoura o timeout e republica a mesma leitura como uma nova mensagem no nível da aplicação. Em planos restritos ele pode nem estar disponível; por exemplo, o plano Serverless gratuito do ConnectMQ suporta QoS até 1, enquanto o plano Serverless pago acrescenta o QoS 2. Veja QoS máximo no MQTT para entender como os brokers anunciam e rebaixam o QoS. Na maioria dos sistemas, QoS 1 com consumidores idempotentes é o design mais robusto e mais barato.
Checklist de solução de problemas
- As duplicatas estão correlacionadas com reconexões? Verifique os logs do client em busca de desconexões e timeouts de keepalive.
- Seu publicador faz retentativas no nível da aplicação além do QoS 1? Isso duplica os caminhos de retentativa.
- Dois clients compartilham um client ID e derrubam um ao outro, causando retomadas de sessão constantes? Veja client MQTT que fica desconectando.
- O consumidor demora para confirmar? Processamento longo dentro do callback atrasa o PUBACK e amplia a janela para reentrega.
- Toda mensagem carrega um ID criado na origem?
Duplicatas e a escolha do seu broker
O ConnectMQ implementa a semântica padrão de QoS 1 e QoS 2, então as recomendações acima se aplicam diretamente. Como os client IDs são vinculados às credenciais emitidas, fica mais fácil evitar o loop de reconexão por client ID duplicado, que muitas vezes causa reentregas em massa. Um broker MQTT gerenciado cuida do armazenamento de sessões e das retentativas para você; a idempotência nos seus consumidores continua sendo responsabilidade sua, em qualquer broker. O tutorial de MQTT é um bom próximo passo para a visão geral.
Comece a usar o ConnectMQ
Crie um broker gratuito em app.connectmq.com no plano Serverless, sem cartão de crédito. Aponte o consumidor com deduplicação para ele usando seu username, password e client ID do ConnectMQ, e seus totais continuam corretos mesmo quando a rede não colabora.
Perguntas frequentes
Por que estou recebendo a mesma mensagem MQTT duas vezes?
Com QoS 1, uma confirmação perdida faz o remetente reenviar. O receptor recebe a mensagem de novo, o que é permitido pela garantia at-least-once.
Posso confiar na flag DUP para descartar duplicatas?
Não. A DUP marca uma retransmissão em um salto, mas a primeira cópia pode nunca ter chegado, e o broker não repassa a flag aos assinantes.
O QoS 0 produz duplicatas?
Não, o QoS 0 nunca é retransmitido pelo protocolo. Em vez disso, ele pode perder mensagens.
Por quanto tempo devo lembrar os IDs de mensagem?
O suficiente para cobrir os atrasos de reconexão e de retomada de sessão, normalmente minutos. Dimensione a partir do seu session expiry interval mais uma margem de segurança.



