Para testar um Last Will do MQTT, você precisa fazer o cliente morrer do jeito errado. O broker só publica o will quando uma conexão termina sem um DISCONNECT correto — então parar o seu app normalmente não prova nada. Mate o processo com kill -9, congele-o com kill -STOP ou corte a rede; depois observe o topic do will a partir de um segundo cliente e anote quando o will chega. Um bom teste também inclui o caso negativo: um desligamento ordenado não pode disparar o will.
Este guia traz um pequeno script de dispositivo com will, um observador, as injeções de falha que acionam cada caminho de código e testes do will delay do MQTT 5.
Quando o broker publica o will
| Evento | Will publicado? | Tempo típico |
|---|---|---|
Queda do processo (kill -9, segfault, OOM kill) | Sim | Quase imediato — o sistema operacional fecha o socket |
| Processo congelado, socket ainda aberto | Sim | Após 1,5 × keep-alive sem tráfego |
| Corte de rede, falta de energia, cabo desconectado | Sim | Após 1,5 × keep-alive (não chega FIN nem RST) |
| Erro de protocolo, broker fecha a conexão | Sim | Imediato |
DISCONNECT ordenado (reason code 0) | Não | — |
DISCONNECT do MQTT 5 com reason 0x04 | Sim | Imediato (mais o will delay) |
| MQTT 5 com Will Delay Interval | Sim, a menos que o cliente reconecte a tempo | Expiração do atraso ou fim da sessão, o que vier primeiro |
A linha do keep-alive explica por que "meu will demora uma eternidade" é tão comum: com um keep-alive de 60 segundos, um dispositivo celular morto é reportado depois de uns 90 segundos. Esse é o comportamento correto, não um bug — veja keep alive para escolher o intervalo. O conceito em si está em Last Will do MQTT.
O dispositivo sob teste
Este script em paho-mqtt 2.x se comporta como um dispositivo bem projetado: registra um will retido offline, publica um online retido ao conectar e, ao receber SIGTERM, desliga de forma ordenada (publica offline por conta própria e desconecta limpo). Assim você tem um único processo com um caminho de saída limpo e vários sujos.
import os, signal, sys, threading
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
DEV = sys.argv[1] if len(sys.argv) > 1 else "dev1"
WILL_DELAY = int(os.environ.get("WILL_DELAY", "0")) # seconds, MQTT 5
STATUS = f"testmqtt/lwt-test/{DEV}/status"
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=f"testmqtt-lwt-{DEV}", protocol=mqtt.MQTTv5)
# client.username_pw_set("username", "password")
will_props = Properties(PacketTypes.WILLMESSAGE)
will_props.WillDelayInterval = WILL_DELAY
client.will_set(STATUS, "offline", qos=1, retain=True, properties=will_props)
def on_connect(client, userdata, flags, reason_code, properties):
print(f"connected: {reason_code}, session present: {flags.session_present}", flush=True)
client.publish(STATUS, "online", qos=1, retain=True)
client.on_connect = on_connect
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())
connect_props = Properties(PacketTypes.CONNECT)
connect_props.SessionExpiryInterval = 300 # must be >= will delay
client.connect("broker.connectmq.com", 1883, keepalive=10,
clean_start=False, properties=connect_props)
client.loop_start()
print("pid", os.getpid(), flush=True)
while not stop.is_set():
stop.wait(1)
# graceful shutdown: report offline ourselves, then a normal DISCONNECT (no will)
client.publish(STATUS, "offline", qos=1, retain=True).wait_for_publish(5)
client.disconnect()
client.loop_stop()Em um segundo terminal, inicie um observador que imprime um timestamp a cada mensagem:
mosquitto_sub -h broker.connectmq.com -p 1883 -t 'testmqtt/lwt-test/#' -F '%I %r %t %p'%I é um timestamp ISO-8601 e %r a flag retain: o will chega como mensagem ao vivo (0), enquanto um offline antigo de uma execução anterior aparece com 1 logo após a assinatura. Você também pode observar testmqtt/lwt-test/# no cliente MQTT online, o que é prático quando você corta a rede da máquina que roda o dispositivo.
Cenário 1: queda com kill -9
python device.py dev1 & # prints: pid 41237
kill -9 41237O observador imprime offline em cerca de um segundo. SIGKILL não pode ser capturado, então o cliente nunca envia DISCONNECT; o kernel fecha a conexão TCP e o broker trata isso como uma desconexão abrupta. Um OOM kill ou um segfault se comportam da mesma forma.
Cenário 2: desligamento ordenado (controle negativo)
python device.py dev1 & PID=$!
sleep 3; kill -TERM "$PID"O observador mostra offline exatamente uma vez — publicado pelo próprio dispositivo, não pelo broker. Se aparecer um segundo offline logo depois, o cliente saiu sem um DISCONNECT limpo (por exemplo, o processo terminou antes de o disconnect ser enviado). Acertar esse caso importa tanto quanto o da queda: um will que dispara a cada deploy enche os dashboards de alarmes falsos.
Cenário 3: congelamento e corte de rede
Um processo congelado mantém o socket aberto, então só o keep-alive consegue detectá-lo:
python device.py dev1 & PID=$!
sleep 3; kill -STOP "$PID" # paho's network thread stops: no PINGREQ
# observer: "offline" after ~15 s (1.5 x keep-alive of 10 s)
kill -CONT "$PID" # the client wakes up, reconnects and publishes "online"Para um corte de rede real, descarte o tráfego na máquina do dispositivo. Rode o observador em outro lugar (ou no navegador, pela porta WebSocket segura), porque estas regras também bloqueiam o seu mosquitto_sub local:
BROKER_IP=$(dig +short broker.connectmq.com | head -1)
sudo iptables -I OUTPUT -p tcp -d "$BROKER_IP" --dport 1883 -j DROP
sudo iptables -I INPUT -p tcp -s "$BROKER_IP" --sport 1883 -j DROP
# ... wait for the will, then restore:
sudo iptables -D OUTPUT -p tcp -d "$BROKER_IP" --dport 1883 -j DROP
sudo iptables -D INPUT -p tcp -s "$BROKER_IP" --sport 1883 -j DROPEm containers, docker pause reproduz o congelamento e docker network disconnect o corte. Para execuções repetíveis no CI, coloque o Toxiproxy entre cliente e broker e use o toxic timeout.
Meça o intervalo entre o último online e o offline na saída do observador. Se for bem maior que 1,5 × o seu keep-alive, o cliente está negociando um keep-alive diferente do que você imagina, ou um proxy ou load balancer no meio mantém a conexão TCP viva do lado do broker.
Cenário 4: will delay do MQTT 5
O Will Delay Interval permite que um dispositivo sobreviva a uma oscilação curta sem se anunciar offline. O broker publica o will quando o atraso expira ou a sessão termina, o que vier primeiro, e o descarta por completo se o cliente reconectar a tempo. Teste os dois resultados:
# A: crash and come back within the delay -> no will
WILL_DELAY=30 python device.py dev2 & PID=$!
sleep 3; kill -9 "$PID"
sleep 10
WILL_DELAY=30 python device.py dev2 & PID=$! # "session present: True", no offline
# B: crash and stay away -> will about 30 s after the broker notices the loss
sleep 3; kill -9 "$PID"Se o teste A ainda gerar um offline, verifique três configurações: a reconexão deve usar o mesmo client ID, o Clean Start deve estar desativado (um clean start encerra a sessão antiga, o que dispara o will na hora) e o session expiry deve ser pelo menos igual ao will delay — com o padrão 0 do MQTT 5 a sessão termina na desconexão e o atraso é ignorado. O guia do will delay interval explica essa interação em detalhes.
Automatize o teste de queda
import signal, subprocess, threading, time
import paho.mqtt.client as mqtt
TOPIC = "testmqtt/lwt-test/ci/status"
def test_will_is_published_on_crash():
got = threading.Event()
def on_message(client, userdata, msg):
if msg.payload == b"offline" and not msg.retain: # live will, not stale state
got.set()
obs = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
obs.on_message = on_message
obs.connect("broker.connectmq.com", 1883)
obs.subscribe(TOPIC, qos=1)
obs.loop_start()
dev = subprocess.Popen(["python", "device.py", "ci"])
time.sleep(3) # connect and publish "online"
dev.send_signal(signal.SIGKILL)
t0 = time.monotonic()
try:
assert got.wait(10), "Last Will not received within 10 s"
print(f"will arrived after {time.monotonic() - t0:.1f} s")
finally:
obs.publish(TOPIC, b"", qos=1, retain=True).wait_for_publish(5) # clear status
obs.disconnect()
obs.loop_stop()Adicione o caso ordenado como um segundo teste que envia SIGTERM e verifica que chega exatamente um offline. Se o teste de queda falhar, siga Last Will não enviado: topic do will errado, uma ACL que nega o topic do will ou um broker que o descarta silenciosamente. Combine o will com uma mensagem online retida, como aqui, para construir uma detecção de presença confiável.
Veja mensagens Last Will ao vivo
Um Last Will só está testado quando você o viu disparar em uma queda, em um congelamento e em um corte de rede — e o viu ficar quieto em um desligamento limpo. Registre os tempos junto das configurações de keep-alive e will delay, e você saberá exatamente quanto tempo os seus dashboards levam para perceber que um dispositivo sumiu.
Perguntas frequentes
Por que o meu Last Will do MQTT não é enviado quando paro o cliente?
Porque o cliente parou de forma ordenada. O Last Will só é publicado quando a conexão termina sem um DISCONNECT normal: uma queda, uma falha de rede ou um timeout de keep-alive. Apertar Ctrl+C em um app que chama disconnect() é uma saída limpa, então o broker descarta o will. Use kill -9 ou corte a rede para testar.
Quanto tempo o broker leva para publicar o Last Will?
Se o processo cliente morre, o sistema operacional fecha o socket e o will é publicado quase imediatamente. Se a rede some silenciosamente, o broker espera até passar 1,5 vez o intervalo de keep-alive sem tráfego. No MQTT 5, um Will Delay Interval acrescenta um atraso adicional.
Por que o meu will é publicado mesmo que o cliente tenha reconectado dentro do will delay?
Porque a sessão terminou. No MQTT 5 o will é publicado quando o atraso expira ou quando a sessão termina, o que acontecer primeiro. Se o session expiry interval for 0, ou se o cliente reconectar com Clean Start ativado, a sessão antiga termina e o will é enviado. Reconecte com o mesmo client ID, Clean Start desativado e um session expiry pelo menos igual ao will delay.

