Um health check de broker MQTT deve provar a única coisa de que seus dispositivos dependem: um cliente consegue conectar, assinar, publicar e receber a própria mensagem em poucos segundos. Qualquer coisa a menos — um ping, uma porta aberta, um processo rodando — pode continuar verde enquanto o broker recusa logins em silêncio ou para de rotear.
Abaixo estão três scripts prontos para uso (Bash, Python, Node.js) que fazem esse round trip e retornam códigos de saída amigáveis para monitoramento, além de exemplos para cron, probes do Kubernetes e alertas.
O que um health check precisa verificar
| Verificação | Detecta | Não detecta |
|---|---|---|
Porta TCP aberta (nc -z) | Host fora do ar, firewall, listener não vinculado | Falhas de autenticação, roteamento quebrado, disco cheio, erros de TLS |
| Processo rodando | Broker que caiu | Tudo o que acontece enquanto ele está "no ar" mas doente |
| Apenas CONNECT | Credenciais, TLS, versão do protocolo, limites de conexão | ACLs de assinatura, roteamento de mensagens |
| Connect + subscribe + publish + receive | Tudo acima, mais roteamento e latência de entrega | Problemas em outros listeners ou outros topics |
A última linha é a que você deve automatizar. Cada execução deve usar um client ID único (duas verificações com o mesmo ID derrubam uma à outra — veja boas práticas de client ID no MQTT) e um topic único, para que uma mensagem atrasada de uma execução anterior nunca faça a execução atual parecer saudável.
Se você quer um diagnóstico pontual mais amplo em vez de um probe recorrente, comece por como verificar o status de um broker MQTT ou explore o broker de forma interativa no cliente MQTT online.
Códigos de saída que as ferramentas de monitoramento entendem
Todos os scripts deste artigo usam a convenção de plugins do Nagios, que Icinga, Zabbix, Checkmk, Sensu e a maioria dos wrappers de cron entendem:
| Código de saída | Estado | Significado nos nossos scripts |
|---|---|---|
0 | OK | O round trip da mensagem teve sucesso dentro do limite de alerta |
1 | WARNING | O round trip teve sucesso, mas foi mais lento que o limite |
2 | CRITICAL | Conexão, assinatura, publicação ou entrega falhou ou expirou |
3 | UNKNOWN | A própria verificação não conseguiu rodar (ferramenta ausente, argumentos inválidos) |
Kubernetes, systemd e cron só distinguem zero de diferente de zero, então os mesmos scripts funcionam em qualquer lugar.
Bash: mosquitto_sub + mosquitto_pub
A verificação mais rápida usa os clientes de linha de comando do Mosquitto (apt install mosquitto-clients ou brew install mosquitto). Um subscriber em segundo plano espera exatamente uma mensagem com timeout, e o publisher envia um token aleatório para o mesmo topic:
#!/usr/bin/env bash
# Round-trip health check. Exit: 0 OK, 2 CRITICAL, 3 UNKNOWN.
HOST="${MQTT_HOST:-broker.connectmq.com}"
PORT="${MQTT_PORT:-8883}"
TIMEOUT="${MQTT_TIMEOUT:-5}"
TOPIC="testmqtt/health/$(hostname)-$$"
TOKEN="ok-$(date +%s)-$RANDOM"
command -v mosquitto_pub >/dev/null || { echo "UNKNOWN - mosquitto clients not installed"; exit 3; }
OPTS=(-h "$HOST" -p "$PORT")
[ "$PORT" = "8883" ] && OPTS+=(--capath /etc/ssl/certs)
[ -n "$MQTT_USER" ] && OPTS+=(-u "$MQTT_USER" -P "$MQTT_PASS")
OUT=$(mktemp)
mosquitto_sub "${OPTS[@]}" -i "health-sub-$$" -t "$TOPIC" -q 1 -C 1 -W "$TIMEOUT" >"$OUT" 2>&1 &
SUB_PID=$!
sleep 1 # give the subscriber time to receive its SUBACK
if ! ERR=$(mosquitto_pub "${OPTS[@]}" -i "health-pub-$$" -t "$TOPIC" -q 1 -m "$TOKEN" 2>&1); then
kill "$SUB_PID" 2>/dev/null
echo "CRITICAL - publish failed: $ERR"; rm -f "$OUT"; exit 2
fi
wait "$SUB_PID"
RECEIVED=$(cat "$OUT"); rm -f "$OUT"
if [ "$RECEIVED" = "$TOKEN" ]; then
echo "OK - round trip via $HOST:$PORT"; exit 0
fi
echo "CRITICAL - no message within ${TIMEOUT}s (${RECEIVED:-no output})"; exit 2Detalhes importantes:
-C 1faz omosquitto_subsair após uma mensagem;-Wfaz com que ele desista após o timeout em vez de travar para sempre.- O script compara o payload com o token em vez de confiar apenas no código de saída, então uma mensagem perdida no topic não pode gerar um falso OK.
- O texto de erro dos clientes (por exemplo
Connection error: Connection Refused: not authorised.) vai parar na linha de saída, que é exatamente o que você quer ver em um alerta. Consulte qualquer reason code na ferramenta de reason codes MQTT. /etc/ssl/certsé o diretório de CAs no Debian, Ubuntu e Alpine. Em outros sistemas, passe--cafilecom o seu bundle de CA.
Python: round trip com limite de latência
A versão em Python (paho-mqtt 2.x) mede o tempo de round trip e retorna WARNING quando ele é mais lento que um limite. Cada etapa tem um prazo, então o script nunca trava um agente de monitoramento:
#!/usr/bin/env python3
"""MQTT round-trip health check. Exit: 0 OK, 1 WARNING, 2 CRITICAL."""
import os, sys, threading, time, uuid
import paho.mqtt.client as mqtt
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "8883"))
TIMEOUT = float(os.getenv("MQTT_TIMEOUT", "5"))
WARN_MS = float(os.getenv("MQTT_WARN_MS", "500"))
topic = f"testmqtt/health/{uuid.uuid4().hex[:12]}"
token = uuid.uuid4().hex
connected, subscribed, received = threading.Event(), threading.Event(), threading.Event()
state = {"connack": None, "suback": None, "t0": 0.0, "rtt": 0.0}
def on_connect(client, userdata, flags, reason_code, properties):
state["connack"] = reason_code
connected.set()
if not reason_code.is_failure:
client.subscribe(topic, qos=1)
def on_subscribe(client, userdata, mid, reason_codes, properties):
state["suback"] = reason_codes[0]
subscribed.set()
if not reason_codes[0].is_failure:
state["t0"] = time.perf_counter()
client.publish(topic, token, qos=1)
def on_message(client, userdata, msg):
if msg.payload.decode() == token:
state["rtt"] = (time.perf_counter() - state["t0"]) * 1000
received.set()
def finish(code, text):
print(text)
sys.exit(code)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"health-{uuid.uuid4().hex[:8]}")
client.on_connect, client.on_subscribe, client.on_message = on_connect, on_subscribe, on_message
if os.getenv("MQTT_USER"):
client.username_pw_set(os.environ["MQTT_USER"], os.getenv("MQTT_PASS"))
if PORT == 8883:
client.tls_set() # system CA bundle
try:
client.connect(HOST, PORT, keepalive=30)
except Exception as exc: # DNS, refused, TLS errors, timeouts
finish(2, f"CRITICAL - cannot connect to {HOST}:{PORT}: {exc}")
client.loop_start()
try:
if not connected.wait(TIMEOUT):
finish(2, "CRITICAL - no CONNACK")
if state["connack"].is_failure:
finish(2, f"CRITICAL - connection refused: {state['connack']}")
if not subscribed.wait(TIMEOUT):
finish(2, "CRITICAL - no SUBACK")
if state["suback"].is_failure:
finish(2, f"CRITICAL - subscribe refused: {state['suback']}")
if not received.wait(TIMEOUT):
finish(2, f"CRITICAL - message not delivered within {TIMEOUT:.0f}s")
rtt = state["rtt"]
level, code = ("WARNING", 1) if rtt > WARN_MS else ("OK", 0)
finish(code, f"{level} - round trip {rtt:.0f} ms | rtt={rtt:.1f}ms;{WARN_MS:.0f}")
finally:
client.disconnect()
client.loop_stop()O texto após | são dados de desempenho do Nagios, então ferramentas que geram gráficos vão plotar o tempo de round trip de graça. Separar falhas de CONNACK, SUBACK e entrega faz diferença às 3 da manhã: "subscribe refused" aponta para uma mudança de ACL, "no CONNACK" para um broker sobrecarregado ou meio morto, e "not delivered" para roteamento ou bridging. Se as assinaturas forem recusadas inesperadamente, ACL do MQTT negada silenciosamente explica as causas mais comuns.
Node.js: MQTT.js 5
A mesma lógica com o MQTT.js 5 e sua API de promises. Um único timer global garante que o processo termine mesmo que algum callback nunca dispare:
import mqtt from 'mqtt';
import { randomUUID } from 'node:crypto';
const url = process.env.MQTT_URL ?? 'mqtts://broker.connectmq.com:8883';
const timeoutMs = Number(process.env.MQTT_TIMEOUT_MS ?? 5000);
const warnMs = Number(process.env.MQTT_WARN_MS ?? 500);
const topic = `testmqtt/health/${randomUUID().slice(0, 12)}`;
const token = randomUUID();
function finish(code, text) {
console.log(text);
process.exit(code);
}
setTimeout(() => finish(2, `CRITICAL - no round trip within ${timeoutMs} ms`), timeoutMs);
const client = mqtt.connect(url, {
clientId: `health-${randomUUID().slice(0, 8)}`,
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
connectTimeout: timeoutMs,
reconnectPeriod: 0, // a health check must not retry silently
});
client.on('error', (err) => finish(2, `CRITICAL - ${err.message}`));
client.on('connect', async () => {
try {
const granted = await client.subscribeAsync(topic, { qos: 1 });
if (!granted[0] || granted[0].qos > 2) finish(2, 'CRITICAL - subscribe refused');
let t0 = 0;
client.on('message', (_topic, payload) => {
if (payload.toString() !== token) return;
const ms = Math.round(performance.now() - t0);
const [code, level] = ms > warnMs ? [1, 'WARNING'] : [0, 'OK'];
client.end(true, () => finish(code, `${level} - round trip ${ms} ms | rtt=${ms}ms;${warnMs}`));
});
t0 = performance.now();
await client.publishAsync(topic, token, { qos: 1 });
} catch (err) {
finish(2, `CRITICAL - ${err.message}`);
}
});Definir reconnectPeriod: 0 é importante: por padrão o MQTT.js reconecta para sempre, o que é ótimo para aplicações e péssimo para um probe que deve falhar rápido. Credenciais erradas aparecem como Connection refused: Not authorized ou Connection refused: Bad username or password, dependendo de como o broker as reporta.
Rodando a verificação de forma agendada
Cron com alertas
# every minute; alert only on non-zero exit
* * * * * /opt/checks/mqtt-health.sh >/tmp/mqtt-health.log 2>&1 || /opt/checks/notify.sh "$(cat /tmp/mqtt-health.log)"A maioria dos agentes de monitoramento (Nagios, Icinga, Checkmk, Zabbix com external checks) consegue rodar qualquer um dos scripts diretamente como plugin. Serviços de uptime com um tipo de monitor MQTT também funcionam, mas um script dá a você controle sobre topics, QoS e limites. Para um dashboard completo, combine o probe com métricas do broker, como descrito em monitoramento de uptime MQTT e monitoramento com Prometheus e Grafana.
Probes do Kubernetes
Para um pod de Mosquitto auto-hospedado, um probe exec que faz um round trip local é muito mais útil que um probe tcpSocket. A imagem oficial eclipse-mosquitto já contém os clientes de linha de comando:
containers:
- name: mosquitto
image: eclipse-mosquitto:2
ports:
- containerPort: 1883
readinessProbe:
tcpSocket:
port: 1883
periodSeconds: 5
livenessProbe:
exec:
command:
- sh
- -c
- >
mosquitto_sub -h 127.0.0.1 -i k8s-probe-sub -t testmqtt/health/k8s -C 1 -W 5 &
sleep 1;
mosquitto_pub -h 127.0.0.1 -i k8s-probe-pub -t testmqtt/health/k8s -m ok;
wait $!
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 8
failureThreshold: 3Se o seu listener exige autenticação, adicione -u/-P a partir de um Secret montado como variáveis de ambiente e libere o usuário do probe nas suas ACLs. Mantenha timeoutSeconds maior que o timeout do próprio script e ajuste failureThreshold para que uma única verificação lenta durante uma pausa de garbage collection não reinicie o broker. Mais detalhes de implantação estão em Mosquitto no Kubernetes.
Monitorando um broker gerenciado
Com um broker gerenciado você não controla o processo, mas continua dono do lado cliente do contrato: seu hostname, suas credenciais, suas ACLs e o caminho de rede a partir dos seus dispositivos. Uma verificação externa de round trip detecta credenciais expiradas, mudanças acidentais de ACL e problemas de rede do seu lado que o monitoramento do próprio provedor não consegue enxergar.
Testando a própria verificação
Um health check que nunca dispara é pior do que nenhum. Antes de confiar nele, force cada modo de falha uma vez:
- Rode normalmente e confirme o código de saída
0:./mqtt-health.sh; echo $?. - Use uma senha errada e confirme
2com uma mensagem "not authorised". - Aponte para uma porta fechada (
MQTT_PORT=1999) e confirme que ele falha dentro do timeout em vez de travar. - Defina
MQTT_WARN_MS=1no script Python ou Node e confirme que o caminho WARNING retorna1. - Acompanhe
testmqtt/health/#no cliente online enquanto a verificação roda para ver o token chegar.
Veja as mensagens do health check ao vivo
Um bom health check de MQTT é pequeno, rápido e honesto: uma conexão, uma assinatura, uma mensagem, um timeout rígido e um código de saída claro. Mantenha o script no seu repositório ao lado da configuração do broker, rode-o de dentro e de fora da sua rede e você vai saber dos problemas do broker antes dos seus dispositivos.
Perguntas frequentes
Verificar se a porta 1883 está aberta é um bom health check de MQTT?
Não. Uma porta TCP aberta só prova que algo está escutando. Um broker pode aceitar conexões TCP e ainda assim rejeitar todo CONNECT, recusar assinaturas ou parar de rotear mensagens. Um health check de verdade conecta, assina, publica e espera pela própria mensagem.
Com que frequência um health check de MQTT deve rodar?
O comum é a cada 30 a 60 segundos para monitoramento externo e a cada 10 a 30 segundos para probes do Kubernetes. Use um client ID e um topic únicos por execução e mantenha o timeout menor que o intervalo, para que as verificações nunca se sobreponham.
Quais códigos de saída um script de health check deve retornar?
Siga a convenção do Nagios: 0 para OK, 1 para WARNING (por exemplo, um round trip lento), 2 para CRITICAL (falha de conexão, assinatura ou entrega) e 3 para UNKNOWN (a própria verificação não conseguiu rodar). Cron, systemd e Kubernetes só se importam com zero versus diferente de zero.


