An MQTT broker health check should prove the one thing your devices depend on: a client can connect, subscribe, publish and receive its own message within a few seconds. Anything less — a ping, an open port, a running process — can stay green while the broker silently refuses logins or stops routing.
Below are three ready-to-use scripts (Bash, Python, Node.js) that perform that round trip and return monitoring-friendly exit codes, plus examples for cron, Kubernetes probes and alerting.
What a health check must verify
| Check | Catches | Misses |
|---|---|---|
TCP port open (nc -z) | Host down, firewall, listener not bound | Auth failures, broken routing, full disks, TLS errors |
| Process running | Crashed broker | Everything that happens while it is "up" but unhealthy |
| CONNECT only | Credentials, TLS, protocol version, connection limits | Subscribe ACLs, message routing |
| Connect + subscribe + publish + receive | All of the above, plus routing and delivery latency | Problems on other listeners or other topics |
The last row is the one to automate. Each run should use a unique client ID (two checks with the same ID kick each other off — see MQTT client ID best practices) and a unique topic, so a delayed message from a previous run can never make the current run look healthy.
If you want a broader one-off diagnosis rather than a recurring probe, start with how to check MQTT broker status or poke the broker interactively in the online MQTT client.
Exit codes that monitoring tools understand
All scripts in this article use the Nagios plugin convention, which Icinga, Zabbix, Checkmk, Sensu and most cron wrappers understand:
| Exit code | State | Meaning in our scripts |
|---|---|---|
0 | OK | Message round trip succeeded within the warning threshold |
1 | WARNING | Round trip succeeded but was slower than the threshold |
2 | CRITICAL | Connect, subscribe, publish or delivery failed or timed out |
3 | UNKNOWN | The check itself could not run (missing tool, bad arguments) |
Kubernetes, systemd and cron only distinguish zero from non-zero, so the same scripts work everywhere.
Bash: mosquitto_sub + mosquitto_pub
The quickest check uses the Mosquitto command-line clients (apt install mosquitto-clients or brew install mosquitto). A background subscriber waits for exactly one message with a timeout, and the publisher sends a random token to the same 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 2Key details:
-C 1makesmosquitto_subexit after one message;-Wmakes it give up after the timeout instead of hanging forever.- The script compares the payload to the token rather than trusting the exit status alone, so a stray message on the topic cannot produce a false OK.
- Error text from the clients (for example
Connection error: Connection Refused: not authorised.) ends up in the output line, which is exactly what you want to see in an alert. Look up any reason code in the MQTT reason code tool. /etc/ssl/certsis the CA directory on Debian, Ubuntu and Alpine. On other systems pass--cafilewith your CA bundle.
Python: round trip with latency threshold
The Python version (paho-mqtt 2.x) measures the round-trip time and returns WARNING when it is slower than a threshold. Every step has a deadline, so the script never blocks a monitoring agent:
#!/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()The text after | is Nagios performance data, so tools that graph it will chart the round-trip time for free. Separating CONNACK, SUBACK and delivery failures matters at 3 a.m.: "subscribe refused" points at an ACL change, "no CONNACK" at an overloaded or half-dead broker, "not delivered" at routing or bridging. If subscriptions are refused unexpectedly, MQTT ACL denied silently explains the usual causes.
Node.js: MQTT.js 5
The same logic with MQTT.js 5 and its promise API. A single global timer guarantees the process exits even if a callback never fires:
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}`);
}
});Setting reconnectPeriod: 0 is important: by default MQTT.js reconnects forever, which is great for applications and terrible for a probe that should fail fast. Bad credentials surface as Connection refused: Not authorized or Connection refused: Bad username or password, depending on how the broker reports them.
Running the check on a schedule
Cron with alerting
# 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)"Most monitoring agents (Nagios, Icinga, Checkmk, Zabbix with external checks) can run any of the scripts directly as a plugin. Uptime services with an MQTT monitor type work too, but a script gives you control over topics, QoS and thresholds. For a full dashboard, combine the probe with broker metrics as described in MQTT uptime monitoring and Prometheus and Grafana monitoring.
Kubernetes probes
For a self-hosted Mosquitto pod, an exec probe that performs a local round trip is far more useful than a tcpSocket probe. The official eclipse-mosquitto image already contains the command-line clients:
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: 3If your listener requires authentication, add -u/-P from a Secret mounted as environment variables, and allow the probe user in your ACLs. Keep timeoutSeconds larger than the script's own timeout, and set failureThreshold so a single slow check during a garbage-collection pause does not restart the broker. More deployment details are in Mosquitto on Kubernetes.
Monitoring a managed broker
With a managed broker you do not control the process, but you still own the client side of the contract: your hostname, your credentials, your ACLs and the network path from your devices. An external round-trip check catches expired credentials, accidental ACL changes and network problems on your side that the provider's own monitoring cannot see.
Testing the check itself
A health check that never fires is worse than none. Before you rely on it, force each failure mode once:
- Run it normally and confirm exit code
0:./mqtt-health.sh; echo $?. - Use a wrong password and confirm
2with a "not authorised" message. - Point it at a closed port (
MQTT_PORT=1999) and confirm it fails within the timeout instead of hanging. - Set
MQTT_WARN_MS=1on the Python or Node script and confirm the WARNING path returns1. - Watch
testmqtt/health/#in the online client while the check runs to see the token arrive.
Watch health-check messages live
A good MQTT health check is small, fast and honest: one connect, one subscription, one message, a strict timeout and a clear exit code. Keep the script in your repository next to your broker configuration, run it from both inside and outside your network, and you will hear about broker problems before your devices do.
Frequently asked questions
Is checking that port 1883 is open a good MQTT health check?
No. An open TCP port only proves that something is listening. A broker can accept TCP connections and still reject every CONNECT, refuse subscriptions or stop routing messages. A real health check connects, subscribes, publishes and waits for its own message.
How often should an MQTT health check run?
Every 30 to 60 seconds is common for external monitoring, and every 10 to 30 seconds for Kubernetes probes. Use a unique client ID and topic per run, and keep the timeout shorter than the interval so checks never overlap.
Which exit codes should a health-check script return?
Follow the Nagios convention: 0 for OK, 1 for WARNING (for example a slow round trip), 2 for CRITICAL (connect, subscribe or delivery failed) and 3 for UNKNOWN (the check itself could not run). Cron, systemd and Kubernetes only care about zero versus non-zero.


