Fundamentos de MQTT

Como Tratar Mensagens MQTT Duplicadas com QoS 1

QoS 1 significa pelo menos uma vez, e às vezes isso significa duas. Entenda por que acontecem mensagens MQTT duplicadas, por que só a flag DUP não resolve e como construir consumidores idempotentes que lidam com elas com segurança.

A mesma mensagem chegando duas vezes a um assinante, ilustrando mensagens duplicadas MQTT com QoS 1
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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:

  1. O dispositivo publica a leitura #42 no broker.
  2. O broker a recebe, a encaminha aos assinantes e envia o PUBACK.
  3. A conexão cai antes de o PUBACK chegar ao dispositivo.
  4. O dispositivo se reconecta com sua sessão e reenvia a #42 com a flag DUP ativada.
  5. 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:

  1. 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.
  2. 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_id transforma duplicatas em conflitos inofensivos. Com um upsert (INSERT ... ON CONFLICT DO NOTHING no 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

  1. As duplicatas estão correlacionadas com reconexões? Verifique os logs do client em busca de desconexões e timeouts de keepalive.
  2. Seu publicador faz retentativas no nível da aplicação além do QoS 1? Isso duplica os caminhos de retentativa.
  3. Dois clients compartilham um client ID e derrubam um ao outro, causando retomadas de sessão constantes? Veja client MQTT que fica desconectando.
  4. O consumidor demora para confirmar? Processamento longo dentro do callback atrasa o PUBACK e amplia a janela para reentrega.
  5. 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.

Artigos relacionados

Por que Seu Cliente MQTT Fica Desconectando (e Como Corrigir)

Loops de reconexão estão entre os problemas mais comuns do MQTT. Conheça as cinco causas habituais, de client IDs duplicados a timeouts de keep alive e pacotes grandes demais, e siga um fluxo de diagnóstico que identifica a sua.

9 min de leitura

Quem Inventou o MQTT? A História dos Oleodutos à IoT

O MQTT foi inventado em 1999 por Andy Stanford-Clark, da IBM, e Arlen Nipper, da Arcom, para monitorar oleodutos via satélite. Acompanhe sua trajetória até o open source, a OASIS, a ISO e os brokers MQTT gerenciados na nuvem de hoje.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.