NanoMQ is the MQTT broker of choice for edge gateways, industrial routers, and embedded Linux systems — but when it crashes or loses its upstream data bridge, the failure is silent. IoT devices keep publishing; messages pile up or are simply dropped; cloud dashboards go stale. Unlike a web server returning a 500 error, a dead MQTT broker just stops routing traffic.
Vigilmon gives you external visibility into NanoMQ through HTTP health probes and heartbeat monitors for your downstream consumers. This tutorial covers NanoMQ process health, MQTT and QUIC port liveness, message throughput, data bridge status, and TLS certificate validity.
Why NanoMQ Needs External Monitoring
NanoMQ's internal metrics are accessible via its built-in HTTP API (enabled in nanomq.conf) — but internal metrics only help if you're actively watching. External monitoring with Vigilmon adds:
- Broker crash detection via TCP port probe — port 1883 going dark means zero MQTT routing
- QUIC transport monitoring — UDP port 14567 failures block roaming and mobile IoT clients
- Data bridge health checks — bridge disconnection means edge data stops reaching the cloud
- TLS certificate expiry alerts — expired certificates on port 8883 silently reject all secure clients
- Multi-region probe consensus to avoid false alarms from transient network blips
Step 1: Enable NanoMQ's Built-In HTTP API
NanoMQ ships with a built-in HTTP management API. Enable it in nanomq.conf:
# nanomq.conf
http_server {
port = 8081
limit_conn = 32
username = admin
password = public
auth_type = basic
}
Restart NanoMQ after editing the config:
systemctl restart nanomq
# or
nanomq start --conf /etc/nanomq/nanomq.conf
Test that the HTTP API is running:
curl -u admin:public http://localhost:8081/api/v4/brokers
You should see broker info including uptime, connections, and version.
Step 2: Build a NanoMQ Health Endpoint
NanoMQ's built-in API returns rich data, but you need a single health endpoint that returns 200 OK when healthy and 503 when degraded. Create a lightweight sidecar using Node.js:
// nanomq-health.js
const express = require('express');
const axios = require('axios');
const app = express();
const NANOMQ_API = process.env.NANOMQ_API_URL || 'http://localhost:8081';
const NANOMQ_USER = process.env.NANOMQ_USER || 'admin';
const NANOMQ_PASS = process.env.NANOMQ_PASS || 'public';
const auth = { username: NANOMQ_USER, password: NANOMQ_PASS };
app.get('/health/nanomq', async (req, res) => {
try {
const [brokersRes, statsRes] = await Promise.all([
axios.get(`${NANOMQ_API}/api/v4/brokers`, { auth, timeout: 5000 }),
axios.get(`${NANOMQ_API}/api/v4/stats`, { auth, timeout: 5000 }),
]);
const broker = brokersRes.data?.data?.[0];
const stats = statsRes.data?.data?.[0];
if (!broker) {
return res.status(503).json({ status: 'down', reason: 'no_broker_data' });
}
const connections = broker.connections || 0;
const msgRateIn = stats?.['messages.received.rate'] || 0;
const msgRateOut = stats?.['messages.sent.rate'] || 0;
return res.status(200).json({
status: 'ok',
uptime: broker.uptime,
connections,
msg_rate_in: msgRateIn,
msg_rate_out: msgRateOut,
version: broker.version,
});
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
// Bridge health endpoint
app.get('/health/nanomq/bridge', async (req, res) => {
try {
const response = await axios.get(`${NANOMQ_API}/api/v4/bridges`, { auth, timeout: 5000 });
const bridges = response.data?.data || [];
const disconnected = bridges.filter(b => b.status !== 'connected');
if (disconnected.length > 0) {
return res.status(503).json({
status: 'degraded',
reason: 'bridge_disconnected',
disconnected: disconnected.map(b => b.name),
});
}
return res.status(200).json({
status: 'ok',
bridges: bridges.length,
all_connected: true,
});
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
// Memory usage endpoint
app.get('/health/nanomq/memory', async (req, res) => {
try {
const response = await axios.get(`${NANOMQ_API}/api/v4/nodes`, { auth, timeout: 5000 });
const node = response.data?.data?.[0];
if (!node) {
return res.status(503).json({ status: 'down', reason: 'no_node_data' });
}
const memUsedMB = Math.round(node.memory_used / 1024 / 1024);
const memTotalMB = Math.round(node.memory_total / 1024 / 1024);
const memPct = memTotalMB > 0 ? Math.round((memUsedMB / memTotalMB) * 100) : 0;
// Alert if memory exceeds 80% — critical on edge hardware
if (memPct > 80) {
return res.status(503).json({
status: 'degraded',
reason: 'high_memory',
memory_used_mb: memUsedMB,
memory_total_mb: memTotalMB,
memory_pct: memPct,
});
}
return res.status(200).json({
status: 'ok',
memory_used_mb: memUsedMB,
memory_total_mb: memTotalMB,
memory_pct: memPct,
});
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
app.listen(3010, () => console.log('NanoMQ health sidecar listening on :3010'));
Python Alternative
# nanomq_health.py
from flask import Flask, jsonify
import requests, os
app = Flask(__name__)
NANOMQ_API = os.environ.get('NANOMQ_API_URL', 'http://localhost:8081')
AUTH = (
os.environ.get('NANOMQ_USER', 'admin'),
os.environ.get('NANOMQ_PASS', 'public'),
)
@app.get('/health/nanomq')
def health():
try:
r = requests.get(f'{NANOMQ_API}/api/v4/brokers', auth=AUTH, timeout=5)
r.raise_for_status()
broker = r.json().get('data', [{}])[0]
return jsonify(status='ok', connections=broker.get('connections', 0),
uptime=broker.get('uptime')), 200
except Exception as e:
return jsonify(status='down', error=str(e)), 503
if __name__ == '__main__':
app.run(port=3010)
Step 3: Configure Vigilmon Monitors for NanoMQ
HTTP Monitor — Broker Health
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL:
https://your-edge-host.example.com/health/nanomq - Set the interval to 1 minute
- Under Expected response:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
5000ms
- Status code:
- Assign a Slack or PagerDuty alert channel
- Save the monitor
TCP Port Monitor — MQTT Port 1883
Add a raw TCP port probe for the MQTT listener — this catches process crashes even if the HTTP sidecar is healthy:
- Create a new monitor, type TCP Port
- Host:
your-edge-host.example.com, Port:1883 - Interval: 1 minute
- Alert channel: P1 (same as broker health)
TCP Port Monitor — QUIC Port 14567
QUIC runs over UDP. Vigilmon's TCP probe won't work for UDP, so probe the QUIC health via your sidecar endpoint. Add a check to nanomq-health.js:
const net = require('net');
const dgram = require('dgram');
app.get('/health/nanomq/quic', async (req, res) => {
// Probe that NanoMQ process is accepting UDP datagrams on 14567
// by sending an initial QUIC packet and expecting a response
const socket = dgram.createSocket('udp4');
const QUIC_PORT = parseInt(process.env.NANOMQ_QUIC_PORT || '14567');
const NANOMQ_HOST = process.env.NANOMQ_HOST || '127.0.0.1';
const timeout = setTimeout(() => {
socket.close();
res.status(503).json({ status: 'down', reason: 'quic_port_timeout' });
}, 3000);
// Send a minimal probe datagram — NanoMQ will reset/reject but the port is open
const msg = Buffer.alloc(1, 0);
socket.send(msg, QUIC_PORT, NANOMQ_HOST, (err) => {
if (err) {
clearTimeout(timeout);
socket.close();
return res.status(503).json({ status: 'down', reason: 'quic_send_failed', error: err.message });
}
// Port accepted the datagram without ECONNREFUSED — it's open
clearTimeout(timeout);
socket.close();
return res.status(200).json({ status: 'ok', quic_port: QUIC_PORT });
});
});
Set up the Vigilmon monitor:
- URL:
https://your-edge-host.example.com/health/nanomq/quic - Expected:
200, body contains"status":"ok" - Interval: 2 minutes
HTTP Monitor — Data Bridge Health
- URL:
https://your-edge-host.example.com/health/nanomq/bridge - Expected:
200, body contains"all_connected":true - Interval: 1 minute
- Alert channel: P1 (bridge disconnection = edge data not reaching cloud)
TLS Certificate Monitor — Port 8883
- Create a new monitor, type TLS Certificate
- Host:
your-edge-host.example.com, Port:8883 - Alert when certificate expires in fewer than 14 days
- Check interval: daily
Step 4: Heartbeat Monitoring for MQTT Subscribers
NanoMQ can be healthy while your downstream MQTT subscriber application is stuck in a reconnect loop or silently dropping messages. Wire a Vigilmon heartbeat into your subscriber to detect this:
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Name:
nanomq-edge-subscriber - Expected interval: 5 minutes
- Grace period: 10 minutes
- Save — copy the heartbeat URL
Node.js MQTT Subscriber with Heartbeat
// subscriber.js
const mqtt = require('mqtt');
const axios = require('axios');
const client = mqtt.connect(process.env.MQTT_URL || 'mqtt://localhost:1883');
const HEARTBEAT_URL = process.env.VIGILMON_HEARTBEAT_URL;
let lastMessageAt = null;
client.on('connect', () => {
client.subscribe(process.env.MQTT_TOPIC || 'sensors/#');
});
client.on('message', async (topic, payload) => {
await processMessage(topic, payload);
lastMessageAt = Date.now();
});
// Send heartbeat every 60 seconds if messages are flowing
setInterval(async () => {
const maxIdleMs = 5 * 60 * 1000;
if (lastMessageAt && Date.now() - lastMessageAt < maxIdleMs) {
await axios.get(HEARTBEAT_URL).catch(() => {});
}
}, 60 * 1000);
// Reconnect detection — log bridge events
client.on('offline', () => console.error('[nanomq] MQTT client offline'));
client.on('reconnect', () => console.warn('[nanomq] MQTT client reconnecting'));
Python MQTT Subscriber with Heartbeat
# subscriber.py
import paho.mqtt.client as mqtt
import requests, os, time, threading
BROKER_HOST = os.environ.get('MQTT_HOST', 'localhost')
BROKER_PORT = int(os.environ.get('MQTT_PORT', 1883))
TOPIC = os.environ.get('MQTT_TOPIC', 'sensors/#')
HEARTBEAT_URL = os.environ['VIGILMON_HEARTBEAT_URL']
last_message_time = None
def on_message(client, userdata, msg):
global last_message_time
process_message(msg.topic, msg.payload)
last_message_time = time.time()
def heartbeat_worker():
while True:
time.sleep(60)
if last_message_time and (time.time() - last_message_time) < 300:
try:
requests.get(HEARTBEAT_URL, timeout=5)
except Exception:
pass
client = mqtt.Client()
client.on_message = on_message
client.connect(BROKER_HOST, BROKER_PORT, 60)
client.subscribe(TOPIC)
threading.Thread(target=heartbeat_worker, daemon=True).start()
client.loop_forever()
Step 5: Alert Routing for Edge IoT Deployments
Edge failures tend to cascade: a NanoMQ crash disconnects all clients simultaneously, the bridge goes down, and cloud dashboards go stale. Configure severity-tiered routing:
| Monitor | Alert Channel | Priority |
|---|---|---|
| Broker health /health/nanomq | Slack + PagerDuty | P1 |
| TCP port 1883 (MQTT) | Slack + PagerDuty | P1 |
| Bridge health /health/nanomq/bridge | Slack + PagerDuty | P1 |
| QUIC port /health/nanomq/quic | Slack | P2 |
| Memory usage /health/nanomq/memory | Slack | P2 |
| Heartbeat: MQTT subscriber | Slack + email | P2 |
| TLS certificate port 8883 | Email | P3 |
Set response time thresholds for early warning on resource-constrained edge hardware:
- Alert at
3000msfor broker health (slow metadata responses signal CPU saturation) - Alert at
4000msfor bridge health (slow bridge queries signal upstream connectivity issues)
Summary
NanoMQ's lightweight footprint makes it ideal for edge deployments — but that same minimalism means failures are silent. External monitoring with Vigilmon catches broker crashes, bridge disconnections, and subscriber stalls before they become extended IoT data gaps:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /health/nanomq | Broker process health, connection count, message rates |
| TCP port monitor on 1883 | Raw MQTT listener availability |
| HTTP monitor on /health/nanomq/bridge | Edge-to-cloud data bridge connectivity |
| HTTP monitor on /health/nanomq/quic | QUIC transport availability for mobile IoT clients |
| TLS certificate monitor on 8883 | Secure MQTT certificate validity |
| Heartbeat monitor | Downstream MQTT subscriber liveness |
Get started free at vigilmon.online — your first NanoMQ monitor is running in under two minutes.