MQTT IoT Monitoring: Track Device Health, Telemetry and Offline Status

Ganesh Velrajan Ganesh Velrajan • 19 Min Read • Published on Nov 12, 2026
MQTT IoT Monitoring: Track Device Health, Telemetry and Offline Status

Short answer: MQTT IoT monitoring means each device publishes its health and sensor data to an MQTT broker, which a collector stores in a time-series database for dashboards and alerts. A practical self-hosted stack is open-source Mosquitto (broker), Telegraf (collector), InfluxDB (storage) and Grafana (dashboards and alerts). For offline detection, combine MQTT’s Last Will and Testament with a heartbeat timeout.

What you’ll build

  • A self-hosted MQTT monitoring stack with Mosquitto, Telegraf, InfluxDB and Grafana, run with Docker Compose
  • A Python device agent that publishes CPU, memory, disk and temperature every 30 seconds
  • Offline detection with retained Last Will messages plus a heartbeat watchdog
  • Broker health monitoring from Mosquitto’s $SYS topics
  • What the monitoring stack can’t do, and how SocketXP adds remote access, OTA updates and fleet management

Get the code: ampaslabs/socketxp-blog-tutorials on GitHub, in the mqtt-iot-monitoring folder (MIT licensed). One docker compose up starts the full stack, with simulated devices, a ready-made Grafana dashboard and failure scenarios you can run.

Everything in this series was tested end to end in Docker with Mosquitto 2.1.2, Telegraf 1.32, InfluxDB 2.7, Grafana 11.3.0, Python 3.12 and paho-mqtt 2.1.0.

What Is MQTT IoT Monitoring?

MQTT (Message Queuing Telemetry Transport) is a lightweight publish/subscribe protocol. Devices publish messages to named topics on a central broker, and any number of subscribers receive them. Publishers and subscribers never talk to each other directly.

For monitoring, that model works well:

  • Push, not poll. Devices send data when they have it. You don’t need to reach into each device, which matters when it sits behind NAT or on a cellular link.
  • Small overhead. The fixed MQTT header is 2 bytes. A 100-byte JSON reading every 30 seconds is only about 0.3 MB of payload per device per day.
  • Built-in liveness. Keepalive and Last Will and Testament (LWT) let the broker announce when a device drops off.
  • Retained messages. A new dashboard or subscriber immediately receives each device’s last known status.
  • Fan-out. One telemetry stream can feed a database, an alerting service and an AI pipeline at the same time.

There are two kinds of MQTT monitoring, and this guide covers both:

  1. Monitoring IoT devices through MQTT: telemetry, heartbeats and offline detection. This is the main focus.
  2. Monitoring the MQTT broker itself: connected clients and message rates, covered in the $SYS section below.

MQTT IoT Monitoring Architecture

  IoT devices (Raspberry Pi, Jetson, gateways, ESP32)
     │  publish  fleet/{site}/{device_id}/telemetry   (JSON, QoS 0)
     │  publish  fleet/{site}/{device_id}/status      (online/offline, QoS 1, retained, LWT)
     ▼
  ┌──────────────────────┐
  │  Mosquitto broker    │──── $SYS/broker/...  (broker health)
  └──────────┬───────────┘
             │ subscribe
     ┌───────┴─────────────────────┐
     ▼                             ▼
  Telegraf                     Offline watchdog (Python)
  (mqtt_consumer)              • LWT "offline" → alert
     │                         • 3 missed heartbeats → alert
     ▼                             │
  InfluxDB  ──►  Grafana           ▼
  (time series)  (dashboards,   fleet/{site}/{device_id}/alerts + webhook
                  alert rules)    (Slack, Teams, PagerDuty)

Each component does one job and can be swapped out. For example, you can use Node-RED instead of Telegraf, TimescaleDB or Prometheus instead of InfluxDB, or EMQX or HiveMQ instead of Mosquitto. This guide uses the most common fully open-source combination.

Step 1: Design Your MQTT Topic Structure

Topic design is the decision that’s hardest to change later. Put the identity in the topic and the measurements in the payload:

TopicPayloadQoSRetainedPublished by
fleet/{site}/{device_id}/telemetryJSON metrics, e.g. {"cpu":12.5,"mem":43.1}0NoDevice, every 30 s
fleet/{site}/{device_id}/statusonline / offline1YesDevice on connect; broker as LWT
fleet/{site}/{device_id}/alertsJSON alert event1NoWatchdog
fleet/{site}/{device_id}/cmdCommand, e.g. reboot1NoOperator or backend

Why this layout works:

  • Wildcards stay simple. fleet/+/+/telemetry matches every device’s telemetry, and fleet/site1/# matches everything at one site.
  • Collectors can tag by topic. Telegraf can turn site and device_id into database tags without parsing the payload.
  • ACLs become one line. A device can be limited to fleet/+/<its-own-id>/#.

Avoid leading slashes (/fleet/...), spaces and personal data in topic names. Never use # as a device’s publish topic.

Step 2: Choosing MQTT QoS for Monitoring

Message typeRecommended QoSWhy
Periodic telemetry0 (at most once)The next reading replaces a lost one. This gives the lowest overhead.
Status / Last Will1 + retainedIt must arrive, and new subscribers need the latest value.
Alerts1 (at least once)A duplicate alert is better than a missed one.
Commands (reboot, config)1, with idempotent handlersMake repeated delivery harmless.

QoS 2 (exactly once) is rarely worth its four-packet handshake for monitoring data.

Step 3: Run Mosquitto, Telegraf, InfluxDB and Grafana with Docker Compose

Create a project directory with this layout:

mqtt-monitoring/
├── .env
├── docker-compose.yml
├── mosquitto/config/
│   ├── mosquitto.conf
│   ├── passwd
│   └── acl
└── telegraf/
    └── telegraf.conf

Put the secrets in .env. Generate your own long random values and never commit this file:

INFLUX_PASSWORD=change-me-influx-admin
INFLUX_TOKEN=change-me-long-random-token
MQTT_TELEGRAF_PASSWORD=change-me-telegraf

docker-compose.yml

services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - mosquitto-data:/mosquitto/data
    restart: unless-stopped

  influxdb:
    image: influxdb:2.7
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_USERNAME: admin
      DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD}
      DOCKER_INFLUXDB_INIT_ORG: iot
      DOCKER_INFLUXDB_INIT_BUCKET: telemetry
      DOCKER_INFLUXDB_INIT_RETENTION: 30d
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN}
    volumes:
      - influxdb-data:/var/lib/influxdb2
    restart: unless-stopped

  telegraf:
    image: telegraf:1.32
    environment:
      INFLUX_TOKEN: ${INFLUX_TOKEN}
      MQTT_TELEGRAF_PASSWORD: ${MQTT_TELEGRAF_PASSWORD}
    volumes:
      - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
    depends_on: [mosquitto, influxdb]
    restart: unless-stopped

  grafana:
    image: grafana/grafana:11.3.0
    ports:
      - "127.0.0.1:3000:3000"   # local only; see "How SocketXP Complements Your MQTT Monitoring Setup" to share it
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on: [influxdb]
    restart: unless-stopped

volumes:
  mosquitto-data:
  influxdb-data:
  grafana-data:

InfluxDB isn’t published on a host port at all. Telegraf and Grafana reach it over the internal Docker network at http://influxdb:8086. Pin image versions in production and upgrade them deliberately.

mosquitto/config/mosquitto.conf

listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
acl_file /mosquitto/config/acl

# Keep retained status messages across broker restarts
persistence true
persistence_location /mosquitto/data/

# Publish broker metrics on $SYS every 10 seconds
sys_interval 10

log_dest stdout

Mosquitto 2.x refuses anonymous clients and listens only on localhost unless you configure a listener, so these settings are required.

mosquitto/config/acl

# Telegraf reads telemetry and broker metrics
user telegraf
topic read fleet/+/+/telemetry
topic read $SYS/#

# The watchdog reads everything under fleet/ and writes alerts
user watchdog
topic read fleet/#
topic write fleet/+/+/alerts

# Every device (username = device ID) can only publish its own topics
pattern write fleet/+/%u/telemetry
pattern write fleet/+/%u/status
pattern read fleet/+/%u/cmd

The %u pattern substitutes the MQTT username. Each device logs in with its device ID as the username, so device01 can publish to fleet/site1/device01/telemetry but cannot spoof device02.

Create the password file with one entry per client. -c creates the file, so use it only for the first entry:

cd mqtt-monitoring
source .env
docker run --rm -v "$PWD/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 \
  mosquitto_passwd -c -b /mosquitto/config/passwd telegraf "$MQTT_TELEGRAF_PASSWORD"
docker run --rm -v "$PWD/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 \
  mosquitto_passwd -b /mosquitto/config/passwd watchdog "change-me-watchdog"
docker run --rm -v "$PWD/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 \
  mosquitto_passwd -b /mosquitto/config/passwd device01 "change-me-device01"
chmod 0700 mosquitto/config/passwd mosquitto/config/acl

Mosquitto 2.1 logs a warning if the password or ACL file is world-readable, and says future versions will refuse to load it, so the chmod matters. To add a device later, run mosquitto_passwd inside the running container, then reload the credentials without dropping connected clients:

docker compose exec mosquitto mosquitto_passwd -b /mosquitto/config/passwd device02 "change-me-device02"
docker compose kill -s HUP mosquitto

Passwords are fine to get started. For production fleets, replace them with per-device X.509 client certificates (mTLS), covered in Step 8.

Step 4: Configure Telegraf to Store MQTT Telemetry in InfluxDB

telegraf/telegraf.conf

[agent]
  interval = "10s"
  flush_interval = "10s"

[[outputs.influxdb_v2]]
  urls = ["http://influxdb:8086"]
  token = "${INFLUX_TOKEN}"
  organization = "iot"
  bucket = "telemetry"

# Device telemetry: fleet/{site}/{device_id}/telemetry  (JSON payload)
[[inputs.mqtt_consumer]]
  servers = ["tcp://mosquitto:1883"]
  topics = ["fleet/+/+/telemetry"]
  qos = 0
  client_id = "telegraf-telemetry"
  username = "telegraf"
  password = "${MQTT_TELEGRAF_PASSWORD}"
  data_format = "json"

  # Turn topic levels into InfluxDB tags: site and device_id
  [[inputs.mqtt_consumer.topic_parsing]]
    topic = "fleet/+/+/telemetry"
    measurement = "_/_/_/measurement"
    tags = "_/site/device_id/_"

# Broker health from Mosquitto's $SYS topics (one numeric value per topic)
[[inputs.mqtt_consumer]]
  servers = ["tcp://mosquitto:1883"]
  topics = [
    "$SYS/broker/clients/connected",
    "$SYS/broker/subscriptions/count",
    "$SYS/broker/load/messages/received/1min",
    "$SYS/broker/load/messages/sent/1min",
  ]
  client_id = "telegraf-sys"
  username = "telegraf"
  password = "${MQTT_TELEGRAF_PASSWORD}"
  name_override = "mosquitto"
  data_format = "value"
  data_type = "float"

Each telemetry message becomes a point in the telemetry measurement. Its numeric JSON keys become fields (cpu, mem, disk, temp, uptime), and site and device_id become tags. Start the stack:

docker compose up -d
docker compose logs -f mosquitto telegraf

You should see Telegraf’s two clients connect as telegraf in the Mosquitto log.

Step 5: Publish Device Telemetry with a Python MQTT Agent

Run this agent on each Linux device, such as a Raspberry Pi, NVIDIA Jetson or industrial gateway. It does three things:

  • Registers a retained Last Will of offline on its status topic.
  • Publishes a retained online status every time it connects or reconnects.
  • Sends CPU, memory, disk, uptime and temperature every 30 seconds.
pip install paho-mqtt psutil

device_agent.py

import json
import os
import signal
import socket
import time

import paho.mqtt.client as mqtt
import psutil

BROKER = os.getenv("MQTT_HOST", "127.0.0.1")
PORT = int(os.getenv("MQTT_PORT", "1883"))
SITE = os.getenv("SITE", "site1")
DEVICE_ID = os.getenv("DEVICE_ID", socket.gethostname())
INTERVAL = int(os.getenv("INTERVAL", "30"))  # seconds between telemetry messages (the heartbeat)

BASE = f"fleet/{SITE}/{DEVICE_ID}"
STATUS_TOPIC = f"{BASE}/status"


def cpu_temperature():
    """Return the CPU temperature in °C, or None if the board doesn't expose it."""
    try:
        temps = psutil.sensors_temperatures()
    except AttributeError:  # not supported on this OS
        return None
    for name in ("cpu_thermal", "coretemp", "soc_thermal"):
        if temps.get(name):
            return temps[name][0].current
    return None


def on_connect(client, userdata, flags, reason_code, properties):
    if not reason_code.is_failure:
        # Overwrite the retained "offline" left by a previous Last Will
        client.publish(STATUS_TOPIC, "online", qos=1, retain=True)


def stop(signum, frame):
    # systemd and `docker stop` send SIGTERM; shut down cleanly like Ctrl+C
    raise KeyboardInterrupt


signal.signal(signal.SIGTERM, stop)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=DEVICE_ID)
client.username_pw_set(DEVICE_ID, os.environ["MQTT_PASSWORD"])
# The broker publishes this if we vanish without a clean disconnect
client.will_set(STATUS_TOPIC, "offline", qos=1, retain=True)
client.on_connect = on_connect
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()  # background network thread with automatic reconnect

try:
    while True:
        payload = {
            "cpu": psutil.cpu_percent(interval=1),
            "mem": psutil.virtual_memory().percent,
            "disk": psutil.disk_usage("/").percent,
            "uptime": int(time.time() - psutil.boot_time()),
            "interval": INTERVAL,
        }
        temp = cpu_temperature()
        if temp is not None:
            payload["temp"] = round(temp, 1)
        client.publish(f"{BASE}/telemetry", json.dumps(payload), qos=0)
        time.sleep(INTERVAL)
except KeyboardInterrupt:
    # A clean disconnect does NOT trigger the Last Will, so announce it ourselves
    client.publish(STATUS_TOPIC, "offline", qos=1, retain=True).wait_for_publish()
    client.disconnect()
    client.loop_stop()

Run it, then watch the topics from another terminal:

MQTT_HOST=broker.local SITE=site1 DEVICE_ID=device01 MQTT_PASSWORD=change-me-device01 \
  python3 device_agent.py

mosquitto_sub -h broker.local -u watchdog -P change-me-watchdog -t 'fleet/#' -v
# fleet/site1/device01/status online
# fleet/site1/device01/telemetry {"cpu": 7.9, "mem": 38.2, "disk": 41.5, "uptime": 86012, "interval": 30, "temp": 48.3}

In production, run the agent as a systemd service with Restart=always so it starts at boot and recovers from crashes. The SIGTERM handler matters here. systemctl stop and docker stop send SIGTERM, not Ctrl+C, and without the handler the agent would exit without publishing offline. In our tests the broker still published the Last Will right away in that case, because the socket closed without a DISCONNECT, but the clean path is more predictable.

No Python on the device? On an ESP32 or other microcontroller, the same pattern applies with an MQTT library such as PubSubClient or ESP-IDF’s esp-mqtt. Set the Last Will in the connect call, publish online with retain, and publish telemetry on a timer. For a complete ESP32 sketch, and Raspberry Pi metrics such as under-voltage and SD card health, see Monitor Raspberry Pi and ESP32 Devices with MQTT.

Step 6: Detect Offline IoT Devices with Last Will and Heartbeats

Offline detection is the most valuable part of MQTT device monitoring, and also the easiest to get wrong. It needs two independent signals. (For MQTT 5 Will Delay, reboot detection and site-outage grouping, see Detect Offline IoT Devices with MQTT Last Will and Heartbeats.)

SignalCatchesDetection time
Last Will and TestamentPower loss, network drop, crash of the MQTT clientAbout 1.5 × keepalive (≈ 90 s with keepalive=60; we measured 87 s)
Heartbeat timeoutDevice still connected but the app is hung or the sensor stopped reportingYour threshold, e.g. 3 missed intervals, plus the alert grace period

How the Last Will works:

  1. The device sends its Will (topic, payload, QoS, retain) inside the CONNECT packet.
  2. If the broker hears nothing from the device within 1.5 × keepalive, or the TCP connection breaks without a DISCONNECT packet, the broker publishes the Will on the device’s behalf.
  3. Because the Will is retained, every later subscriber also sees offline until the device reconnects and publishes online.

A clean disconnect() deletes the Will without sending it. That’s why the agent above publishes offline itself before shutting down.

A connected but hung process keeps answering keepalive pings from the network thread, so the Last Will never fires. The heartbeat timeout covers that case. This small watchdog handles both signals:

watchdog.py

import json
import os
import threading
import time
import urllib.request

import paho.mqtt.client as mqtt

BROKER = os.getenv("MQTT_HOST", "127.0.0.1")
WEBHOOK_URL = os.getenv("WEBHOOK_URL")  # optional: Slack, Teams or any HTTP endpoint
INTERVAL = 30                    # must match the device telemetry interval
STALE_AFTER = 3 * INTERVAL + 10  # three missed heartbeats, plus some slack

last_seen = {}   # "fleet/site/device" -> last telemetry timestamp
alerted = set()  # devices with an open alert
lock = threading.Lock()


def raise_alert(client, base, state, reason):
    event = {"device": base, "state": state, "reason": reason, "ts": int(time.time())}
    client.publish(f"{base}/alerts", json.dumps(event), qos=1)
    print("ALERT", event)
    if WEBHOOK_URL:
        req = urllib.request.Request(
            WEBHOOK_URL,
            data=json.dumps({"text": f"{base} is {state}: {reason}"}).encode(),
            headers={"Content-Type": "application/json"},
        )
        try:
            urllib.request.urlopen(req, timeout=5)
        except OSError as err:
            print("webhook failed:", err)


def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe("fleet/+/+/telemetry", qos=0)
    client.subscribe("fleet/+/+/status", qos=1)


def on_message(client, userdata, msg):
    base, kind = msg.topic.rsplit("/", 1)
    with lock:
        if kind == "telemetry":
            last_seen[base] = time.time()
            if base in alerted:
                alerted.discard(base)
                raise_alert(client, base, "recovered", "telemetry resumed")
        elif kind == "status" and msg.payload == b"offline" and not msg.retain:
            # Skip retained messages so a watchdog restart doesn't re-alert old outages
            if base not in alerted:
                alerted.add(base)
                raise_alert(client, base, "offline", "connection lost (Last Will)")


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="watchdog")
client.username_pw_set("watchdog", os.environ["MQTT_PASSWORD"])
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, keepalive=60)
client.loop_start()

while True:
    now = time.time()
    with lock:
        for base, seen in last_seen.items():
            if now - seen > STALE_AFTER and base not in alerted:
                alerted.add(base)
                raise_alert(client, base, "stale", f"no telemetry for {int(now - seen)}s")
    time.sleep(10)

Tuning tips:

  • Choose keepalive based on the link. Use 60 s on Ethernet or Wi-Fi. On cellular, 120–300 s saves data and battery, but offline detection is slower.
  • Avoid alert storms. A site-wide network outage will take many devices offline at once. Group alerts by site before paging anyone.
  • Track flapping. Count online/offline transitions per hour. A device that reconnects every few minutes usually has a power, signal or memory-leak problem.

Step 7: Build Grafana Dashboards and Alerts

Open Grafana at http://localhost:3000 (default login admin / admin, which you’ll be asked to change) and add a data source:

  • Type: InfluxDB, Query language: Flux
  • URL: http://influxdb:8086
  • Organization: iot, Token: your INFLUX_TOKEN, Default bucket: telemetry

CPU usage per device:

from(bucket: "telemetry")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "telemetry" and r._field == "cpu")
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)

Because device_id is a tag, Grafana draws one series per device automatically. Duplicate the panel for mem, disk and temp.

Broker health (connected clients and message rate):

from(bucket: "telemetry")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "mosquitto")
  |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: true)
  |> fill(usePrevious: true)   // $SYS values are only published when they change
Grafana dashboard for MQTT IoT monitoring showing devices reporting, online/offline timeline, CPU, memory, disk, last seen, broker health and ESP32 heap

The MQTT IoT Fleet dashboard from the companion repo, running against live data from simulated devices. The red blocks in the timeline are from two broker restarts during testing.

For a full fleet dashboard, with site and device dropdowns, a status timeline, a last-seen table, provisioning and downsampling, see MQTT Grafana Dashboard with Mosquitto, Telegraf and InfluxDB.

Useful Grafana alert rules:

AlertConditionSuggested threshold
Device not reportingQuery returns No Data for a deviceFor 5 minutes
High CPUcpu mean> 90 % for 10 minutes
Disk filling updisk last> 85 %
Overheatingtemp last> 80 °C (Raspberry Pi starts throttling at about 80–85 °C)
Broker client dropclients/connected falls sharply> 20 % drop in 5 minutes

Setting the No Data state to Alerting gives you a second, dashboard-level offline check alongside the watchdog.

Monitoring the MQTT Broker with $SYS Topics

When the broker goes down, all monitoring goes with it, so watch the broker too. Mosquitto publishes its own metrics under $SYS/broker/. It checks them every sys_interval seconds and only publishes values that changed, so a steady client count produces few data points. Don’t set a “no data” alert on these series:

$SYS topicWhat it tells you
$SYS/broker/clients/connectedDevices currently connected. A sudden drop usually means a network or site outage.
$SYS/broker/subscriptions/countTotal active subscriptions
$SYS/broker/load/messages/received/1minInbound message rate. A drop means devices stopped publishing.
$SYS/broker/load/messages/sent/1minOutbound rate to subscribers
$SYS/broker/uptimeSeconds since the broker started. A reset means it restarted.

Check them by hand with:

mosquitto_sub -h broker.local -u telegraf -P "$MQTT_TELEGRAF_PASSWORD" -t '$SYS/broker/#' -v

Note that # and + at the start of a subscription do not match $SYS topics, so you have to subscribe to $SYS/... explicitly.

Grafana chart of Mosquitto $SYS broker metrics: connected clients, subscriptions and messages received and sent per minute

Mosquitto $SYS broker metrics in Grafana. The dips around 14:28 and 14:29 are broker restarts; connected clients and subscriptions recover right away.

Step 8: Secure MQTT Monitoring with mTLS

Port 1883 sends usernames, passwords and telemetry in plain text. Before devices connect over any network you don’t fully control:

  1. Switch to TLS on port 8883.
  2. Replace passwords with per-device X.509 client certificates (mutual TLS). A leaked password can be reused from anywhere, while a certificate is bound to a private key that never leaves the device.
  3. Set use_identity_as_username true in Mosquitto so the certificate CN becomes the username. The %u ACL patterns above then work unchanged.

The SocketXP CLI can issue CA-signed broker and device certificates with a single socketxp ca login command, so you don’t need to run your own OpenSSL CA. Follow the step-by-step guide: MQTT mTLS Authentication Setup with Client Certificates.

How SocketXP Complements Your MQTT Monitoring Setup

The stack above is good at one job: watching. It collects telemetry, charts it and tells you when something is wrong. It can’t act on what it finds. When Grafana shows device01 at 98% disk, or the watchdog reports a device stuck in a reboot loop, the MQTT stack can’t log in to the device, copy its logs off, or ship it a fix.

SocketXP is an IoT device management and remote access platform that covers that second half: remote access, OTA updates and fleet management for the same Linux devices you’re monitoring. It runs alongside your MQTT stack and doesn’t replace it.

MQTT monitoring stack vs SocketXP

What you needSelf-hosted MQTT monitoring stackSocketXP
Custom sensor and app telemetry, Grafana dashboards✅ Its core strengthNot the focus; keep using MQTT + Grafana
Detect offline or unhealthy devices✅ Last Will, heartbeats, alert rules✅ Independent agent-based online/offline, CPU, memory, disk, process and log-pattern alerts
Log in to a device to troubleshoot❌✅ SSH, VNC, RDP or a browser terminal, even behind NAT, CGNAT or cellular, with no VPN or port forwarding
Pull logs and config files off a device❌✅ SFTP/SCP and a web file browser
Run a command across many devicesOnly if you build your own cmd topic handlers✅ Remote command execution across a device group or the whole fleet
Deploy a fix or new version❌✅ OTA updates for firmware, application binaries, Debian packages, Docker images and AI models
Roll out updates safely❌✅ Staged (canary) rollouts, maintenance-window scheduling, rollback through your install script’s health check, per-device status and an audit log
Per-device identity and certificatesBuild and run your own OpenSSL CA✅ Built-in PKI CA: per-device X.509 certificates, mutual TLS, per-device revocation
Edge AI devices calling a private LLM❌✅ SocketXP AI Gateway

From alert to fix: how the two work together

  1. Detect. Grafana or the watchdog flags a problem, for example site2/device03 hasn’t reported for five minutes, or its disk is filling up.
  2. Confirm. SocketXP’s own agent shows whether the whole device is down or only the MQTT client is. The two systems are independent, so you can tell those cases apart.
  3. Get in. SSH into the device through SocketXP, even though it sits behind a cellular modem with no public IP. Read journalctl, check vcgencmd get_throttled, free up disk space or restart the service.
  4. Fix the fleet, not just one device. If the root cause is a bug, package the fix and push it with an OTA update: a pilot group first, then the rest of the fleet once its health checks pass.
  5. Verify. Watch the same Grafana panels recover, and the fw tag in the last-seen table change device by device.

MQTT monitoring covers steps 1 and 5. SocketXP covers steps 2 to 4.

OTA updates for the devices you monitor

OTA is the capability the monitoring stack misses most. Without it, every fix found through a dashboard ends with someone visiting the device or improvising a script over SSH. SocketXP delivers any artifact you can install with a shell script: firmware, binaries, .deb packages, Docker images, even the monitoring agent from this guide. It also tracks success or failure per device. See OTA updates for IoT and the OTA update guide.

AIoT: edge AI devices

The same pairing works for edge AI fleets such as NVIDIA Jetson, or a Raspberry Pi with an AI accelerator. MQTT carries inference_ms, fps and model_version into Grafana. SocketXP rolls out new models OTA to a pilot group first, so you can compare inference latency on the dashboard before the full rollout. Devices that need to call a private LLM reach it through the SocketXP AI Gateway, without exposing the model server to the internet.

Also useful: secure connectivity for the stack itself

The same agent can carry the monitoring traffic too. Devices behind NAT can reach a private Mosquitto broker without opening port 1883 or 8883 (see Remote Access MQTT Broker Behind NAT or Firewall). You can also share the local-only Grafana with your team using socketxp connect http://localhost:3000.

SocketXP is available as a cloud service or self-hosted. The self-hosted Enterprise edition is for commercial fleets. A free Community edition covers non-commercial use with up to 100 devices and community-forum support. Either one fits the same keep-it-on-your-own-servers approach as this MQTT stack.

MQTT vs HTTP for IoT Monitoring

MQTTHTTP polling / webhooks
ConnectionOne persistent connection per deviceNew request per reading (or keep-alive)
Header overhead2-byte minimum fixed headerHundreds of bytes of headers
DirectionBidirectional (telemetry up, commands down)Mostly device → server
Offline detectionBuilt in (keepalive + Last Will)Must be built with timeouts
Latest value for new clientsRetained messagesMust query a database
Works behind NATYes, the device dials outYes for push, no for polling the device
Best forFleets, frequent telemetry, constrained linksOccasional reports, simple integrations

Production Checklist

  • Anonymous access disabled; per-device credentials or certificates; ACLs scoped with %u
  • TLS on 8883, with mTLS for device identity
  • Retained status topic with a Last Will on every device
  • Heartbeat watchdog plus a Grafana No Data alert
  • Broker $SYS metrics charted and alerted
  • InfluxDB retention policy set (30 days of raw data in this guide), with downsampling for longer history
  • Persistence enabled on Mosquitto so retained status survives restarts
  • Device agent running under systemd with Restart=always
  • Remote access path ready for when an alert needs hands-on troubleshooting

Next Steps

Frequently Asked Questions

  1. What is MQTT IoT monitoring?

    MQTT IoT monitoring means IoT devices publish health and sensor telemetry (CPU, memory, temperature, sensor readings, online status) to an MQTT broker. Collectors subscribe to those topics, store the data in a time-series database, and drive dashboards and alerts. MQTT suits this because it is lightweight, push-based and works over unreliable networks.

  2. How do I know if an MQTT device is offline?

    Use two signals. First, each device registers a retained Last Will and Testament (LWT) message such as 'offline' on its status topic. The broker publishes it automatically if the device disconnects ungracefully or misses its keepalive (after 1.5 times the keepalive interval). Second, track the last telemetry timestamp and flag a device that misses three consecutive intervals, which catches devices that are connected but hung.

  3. Which MQTT QoS level should I use for IoT telemetry?

    Use QoS 0 for frequent periodic telemetry, where the next reading replaces a lost one. Use QoS 1 with retain=true for status and Last Will messages, and QoS 1 for alerts and commands that must arrive at least once. QoS 2 is rarely needed for monitoring and adds overhead.

  4. Is Mosquitto good for IoT monitoring?

    Yes. Eclipse Mosquitto is a lightweight, fully open-source MQTT broker that runs on anything from a Raspberry Pi to a cloud VM. It is only a broker, so for monitoring you pair it with a collector such as Telegraf or Node-RED, a time-series database such as InfluxDB, and a dashboard tool such as Grafana.

  5. How do I monitor the Mosquitto broker itself?

    Mosquitto publishes its own metrics on $SYS topics. It checks them every sys_interval seconds (10 seconds by default) and publishes the values that changed, including $SYS/broker/clients/connected, $SYS/broker/subscriptions/count and $SYS/broker/load/messages/received/1min. Subscribe to them with Telegraf and chart them in Grafana next to your device metrics.

  6. MQTT vs HTTP for IoT monitoring: which is better?

    MQTT is usually better for monitoring device fleets. Devices keep one persistent connection and push data with a 2-byte minimum header, instead of opening an HTTP request per reading. MQTT also has built-in offline detection with Last Will and keepalive, and retained messages that give new subscribers the latest status immediately.

  7. What does SocketXP add to an MQTT monitoring setup?

    MQTT monitoring collects telemetry and raises alerts, but it can't act on them. SocketXP is an IoT device management and remote access platform that adds the missing half: SSH, VNC, RDP and browser access to devices behind NAT or cellular networks, file transfer, remote commands across the fleet, OTA updates with staged rollouts and rollback, per-device certificates, and independent device health alerts. Keep MQTT and Grafana for telemetry, and use SocketXP to fix and update the devices.

  8. What should I do when MQTT monitoring shows a device is unhealthy?

    MQTT tells you something is wrong, but it cannot fix the device. Use a device management and remote access platform such as SocketXP to SSH into the device behind NAT, read logs and restart services. If the fix applies to many devices, push it as an OTA update to a pilot group first, then the rest of the fleet.

SocketXP IoT Remote Access and Device Management Platform

Remotely access, manage, and update your IoT & AIoT edge fleet with SocketXP's secure and scalable platform.

Start Your Free Trial Now!

Join thousands of satisfied users who trust SocketXP for a secure, reliable, and scalable IoT Edge device management solution. Start your free trial now.