Raspberry Pi

Publish Raspberry Pi System Stats over MQTT for Fleet Health Monitoring

A small Python service that publishes CPU temperature, load, memory, disk usage and under-voltage flags from every Raspberry Pi to MQTT, with a last will so you know when a Pi goes offline.

Several Raspberry Pi boards reporting CPU temperature and memory as raspberry pi system stats mqtt messages to a central broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Publishing raspberry pi system stats mqtt messages is the simplest way to know, at a glance, whether every Pi you run is healthy. In this tutorial you will write a Python service that reports CPU temperature, load, memory, disk usage, uptime and power throttling flags on a fixed interval, announces itself online with a retained status and lets the broker mark it offline through a last will. The same script runs unchanged on one Pi or fifty.

Why Monitor Raspberry Pis over MQTT

Pis in the field fail in predictable ways: SD cards fill up with logs, cases trap heat and the CPU throttles, cheap power supplies cause under-voltage, and Wi-Fi drops leave a device silently unreachable. Traditional monitoring stacks poll each machine, which means opening ports or running an agent that needs inbound access. MQTT flips that around. Each Pi makes one outbound connection to a broker and pushes its own numbers, so it works behind NAT, on mobile routers and on networks you do not control.

The broker also gives you presence for free. With a retained online status and a last will of offline, any dashboard that subscribes sees the current state of every Pi immediately, and the broker updates it when a device disappears. If the mechanics are unfamiliar, read what an MQTT broker does first.

The Metrics and Where They Come From

Metric Source Why it matters
cpu_temp /sys/class/thermal/thermal_zone0/temp Throttling starts as temperature climbs
load1, load5 os.getloadavg() Sustained load above core count means a backlog
cpu_pct psutil.cpu_percent() Short-term CPU usage
mem_pct psutil.virtual_memory() Memory leaks in long-running services
disk_pct psutil.disk_usage("/") Full root file systems break everything
uptime_s psutil.boot_time() Unexpected reboots show as resets
throttled vcgencmd get_throttled Under-voltage and thermal throttling flags
wifi_rssi /proc/net/wireless Weak signal explains dropouts

The get_throttled value is a bit field. Bit 0 means under-voltage right now, bit 2 means currently throttled, and bits 16 and 18 mean those conditions have occurred since boot. A non-zero value is worth an alert on its own, because it almost always points to a power supply problem that will eventually corrupt the SD card. The Raspberry Pi documentation lists every bit.

Topic Design for a Fleet

Use one subtree per device, with the hostname as the device ID. For testing on the free public MQTT broker, add a random segment so nobody else's data collides with yours:

connectmq/demo/a9e217/fleet/<hostname>/stats     JSON, every 30 s, not retained
connectmq/demo/a9e217/fleet/<hostname>/status    "online" / "offline", retained, last will

A monitoring client then subscribes to connectmq/demo/a9e217/fleet/+/status and connectmq/demo/a9e217/fleet/+/stats and discovers new Pis automatically when they first publish.

Install the Dependencies

On each Pi, create a virtual environment so the service does not depend on system Python packages:

sudo apt update && sudo apt install -y python3-venv
python3 -m venv ~/pistats-env
~/pistats-env/bin/pip install "paho-mqtt>=2.0" psutil

The Raspberry Pi System Stats MQTT Publisher

Save this as ~/pistats.py:

#!/usr/bin/env python3
"""Publish Raspberry Pi health metrics over MQTT."""
import json
import os
import socket
import ssl
import subprocess
import time

import paho.mqtt.client as mqtt
import psutil

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")          # YOUR_USERNAME in production
PASSWORD = os.getenv("MQTT_PASSWORD")          # YOUR_PASSWORD in production
HOSTNAME = socket.gethostname()
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"pistats-{HOSTNAME}-a9e217")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/a9e217/fleet")
INTERVAL = int(os.getenv("INTERVAL", "30"))

STATS_TOPIC = f"{PREFIX}/{HOSTNAME}/stats"
STATUS_TOPIC = f"{PREFIX}/{HOSTNAME}/status"


def cpu_temp():
    try:
        with open("/sys/class/thermal/thermal_zone0/temp") as f:
            return round(int(f.read().strip()) / 1000, 1)
    except (OSError, ValueError):
        return None


def throttled():
    try:
        out = subprocess.run(["vcgencmd", "get_throttled"],
                             capture_output=True, text=True, timeout=2).stdout
        return int(out.strip().split("=")[1], 16)
    except (OSError, IndexError, ValueError, subprocess.TimeoutExpired):
        return None


def wifi_rssi():
    try:
        with open("/proc/net/wireless") as f:
            lines = f.readlines()[2:]
        for line in lines:
            parts = line.split()
            if parts and parts[0].startswith("wlan"):
                return int(float(parts[3]))
    except (OSError, IndexError, ValueError):
        pass
    return None


def collect():
    load1, load5, _ = os.getloadavg()
    t = throttled()
    return {
        "ts": int(time.time()),
        "cpu_temp": cpu_temp(),
        "cpu_pct": psutil.cpu_percent(interval=None),
        "load1": round(load1, 2),
        "load5": round(load5, 2),
        "mem_pct": psutil.virtual_memory().percent,
        "disk_pct": psutil.disk_usage("/").percent,
        "uptime_s": int(time.time() - psutil.boot_time()),
        "throttled": t,
        "undervoltage_now": bool(t & 0x1) if t is not None else None,
        "undervoltage_seen": bool(t & 0x10000) if t is not None else None,
        "wifi_rssi": wifi_rssi(),
    }


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"Connect failed: {reason_code}")
        return
    print(f"Connected to {HOST}:{PORT} as {CLIENT_ID}")
    client.publish(STATUS_TOPIC, "online", qos=1, retain=True)


def on_disconnect(client, userdata, flags, reason_code, properties):
    print(f"Disconnected: {reason_code}, paho will reconnect")


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=CLIENT_ID, protocol=mqtt.MQTTv5)
if USERNAME:
    client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
    client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
client.will_set(STATUS_TOPIC, "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.reconnect_delay_set(min_delay=2, max_delay=60)

client.connect_async(HOST, PORT, keepalive=60)
client.loop_start()
psutil.cpu_percent(interval=None)  # prime the counter

try:
    while True:
        time.sleep(INTERVAL)
        if client.is_connected():
            client.publish(STATS_TOPIC, json.dumps(collect()), qos=0)
finally:
    client.publish(STATUS_TOPIC, "offline", qos=1, retain=True).wait_for_publish(3)
    client.disconnect()
    client.loop_stop()

A few deliberate choices: stats use QoS 0 because a missing sample every now and then does not matter and the next one arrives shortly. Status uses QoS 1 and retain because presence must be correct. connect_async with loop_start means the script starts even if the network is not up yet at boot, and paho keeps retrying in the background. On a clean shutdown the finally block publishes offline explicitly, because the last will only fires on an unexpected disconnect.

Run it once by hand and watch the output from another machine:

~/pistats-env/bin/python ~/pistats.py
mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-a9e217 \
  -t 'connectmq/demo/a9e217/fleet/#' -v

Pull the Pi's network cable and, after roughly one and a half keepalive intervals, the broker publishes offline on its behalf. That is the MQTT last will doing its job.

Run It as a systemd Service

Create an environment file so credentials stay out of the script:

sudo tee /etc/pistats.env >/dev/null <<'EOF'
MQTT_HOST=broker.connectmq.com
MQTT_PORT=1883
INTERVAL=30
EOF
sudo chmod 600 /etc/pistats.env
sudo tee /etc/systemd/system/pistats.service >/dev/null <<'EOF'
[Unit]
Description=Publish Raspberry Pi stats over MQTT
After=network-online.target
Wants=network-online.target

[Service]
User=pi
EnvironmentFile=/etc/pistats.env
ExecStart=/home/pi/pistats-env/bin/python /home/pi/pistats.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now pistats
journalctl -u pistats -f

Adjust the user and paths to match your account. For more patterns around boot-time scripts, see running a Python MQTT script on boot.

Moving the Fleet to a Private Broker

System stats reveal hostnames, uptime and network details, so a real fleet should not report to a public broker. With ConnectMQ, a cloud MQTT broker, you create a broker in the portal and receive credentials. Change only the environment file:

MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
MQTT_PREFIX=site1/fleet

ConnectMQ binds client IDs to issued credentials, so each Pi needs its own set from the portal. Port 8883 uses TLS with a Let's Encrypt certificate that the Pi already trusts. The free Serverless plan allows up to 25 connections, which covers a sizeable home or lab fleet; the paid Serverless plan goes much further.

ConnectMQ's portal also helps with the monitoring side without extra software. The live topic explorer shows every Pi's latest stats, and threshold alert rules can flag a value such as cpu_temp crossing a limit, with an alerts log to review afterwards.

Choosing Alert Thresholds That Mean Something

Collecting numbers is easy; deciding which ones deserve a notification is where fleet monitoring earns its keep. Alerts that fire constantly get ignored, so start with a short list of conditions that almost always indicate a real problem:

  • Under-voltage seen since boot. Any non-zero undervoltage_seen points to a weak power supply or a thin USB cable. Fix it before it corrupts the SD card.
  • CPU temperature staying high. Short spikes during updates are normal. A temperature that stays near the throttling point for many minutes means a blocked vent, a missing heatsink or a case in direct sunlight.
  • Disk usage above roughly 90 percent. Logs and container images grow quietly. A full root file system breaks package updates, databases and sometimes the boot itself.
  • Uptime resetting unexpectedly. If uptime_s drops without a planned reboot, the Pi crashed or lost power, and the cause is usually power or heat.
  • Offline for longer than a few intervals. Combine the retained status with the age of the last stats message so a brief Wi-Fi blip does not page anyone.

Load average and memory are better watched as trends than as alerts. A slow climb over days usually reveals a leaking service long before it causes trouble.

Using the Data

Once the numbers flow, choose how much tooling you want:

  1. Quick look: ConnectMQ's topic explorer or any MQTT desktop client.
  2. Wall display: the Raspberry Pi kiosk dashboard can show one tile per Pi.
  3. History and graphs: Telegraf into InfluxDB with Grafana on top, as in the Raspberry Pi Grafana and InfluxDB tutorial.

Troubleshooting

Symptom Cause Fix
throttled is null vcgencmd missing or no permission Install libraspberrypi-bin, add user to video group
cpu_pct always 0.0 on first sample Counter not primed The script primes it before the loop
Status flips online and offline repeatedly Two Pis with the same client ID Give each Pi a unique client ID
offline never appears after unplugging Waiting for keepalive timeout Wait about 1.5 times the keepalive
wifi_rssi is null on Ethernet No wireless interface Expected, ignore or remove the field

Get Started with ConnectMQ

Bring every Pi's health into one place: sign up at app.connectmq.com, create a free Serverless broker without a credit card and drop the credentials into /etc/pistats.env. For the full series of Pi projects, start at the Raspberry Pi MQTT tutorial.

Frequently Asked Questions

How often should a Raspberry Pi publish system stats?

Every 30 to 60 seconds is plenty for health monitoring. Faster intervals add traffic without revealing much more, since temperature and disk usage change slowly.

Does publishing stats slow the Raspberry Pi down?

No meaningful amount. Reading a few files and sending a small JSON message every 30 seconds is a negligible load even on a Pi Zero 2 W.

How do I know when a Pi goes offline?

Subscribe to the retained status topic. The script sets a last will of offline, so the broker publishes it automatically when the Pi disconnects unexpectedly.

Can I add GPU temperature or custom metrics?

Yes, add fields to the collect() dictionary. On current Pi models the CPU and GPU share a die, so the thermal zone value already reflects both.

Related articles

Pico W vs Pico 2 W for MQTT: RP2040 vs RP2350 Performance, Memory and TLS

The Pico 2 W keeps the Pico W's footprint and Wi-Fi chip but swaps the RP2040 for the faster RP2350. This comparison looks at what that means for MQTT devices: CPU, memory, TLS, security features, software support, and which board to buy for which project.

8 min read

Raspberry Pi RFID MQTT Access Logging with the RC522 Reader

Wire an RC522 RFID reader to a Raspberry Pi, read tags in Python, check them against a local allowlist and publish every access attempt as an MQTT event that loggers, dashboards and alerting tools can consume.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.