Raspberry Pi

Raspberry Pi MQTT Tutorial: Publish, Subscribe and Automate with Python

A complete Raspberry Pi MQTT tutorial: set up a Python virtual environment, publish CPU and sensor readings, switch an LED over MQTT, run the script as a service and move to TLS with your own credentials.

Raspberry Pi board wired to an LED and a sensor publishing data to a cloud broker in a Raspberry Pi MQTT tutorial
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

This Raspberry Pi MQTT tutorial takes you from a fresh Raspberry Pi OS install to a Python program that publishes sensor readings, listens for commands and switches a GPIO pin from anywhere. You will test everything against the free public broker at broker.connectmq.com, then move the same code to a private, TLS-secured broker with a few environment variables. Along the way you will learn the patterns that keep a Pi-based IoT device reliable for months, not minutes.

What You Will Build

By the end of this guide your Raspberry Pi will run a single Python service that does four things:

  1. Connects to an MQTT broker and reconnects automatically after Wi-Fi drops or reboots.
  2. Publishes a JSON telemetry message every ten seconds with the CPU temperature, load average and, optionally, a room sensor.
  3. Subscribes to a command topic and switches an LED (or relay) on GPIO17.
  4. Publishes a retained online/offline status, using an MQTT last will so other clients know when the Pi disappears.

The topic layout keeps everything under one unique prefix, which matters on a shared public broker:

Topic Direction Example payload Retained
connectmq/demo/<deviceId>/telemetry Pi publishes {"cpu_temp":48.3,"load":0.21} No
connectmq/demo/<deviceId>/led/set Pi subscribes ON, OFF, TOGGLE No
connectmq/demo/<deviceId>/led/state Pi publishes ON or OFF Yes
connectmq/demo/<deviceId>/status Pi publishes + last will online or offline Yes

If terms such as topic, retained message or last will are new, skim what an MQTT broker does before you start. The short version: the broker is a post office, your Pi and your laptop are clients, and topics are the addresses.

Prerequisites

You need very little hardware for this Raspberry Pi MQTT tutorial:

  • Any Raspberry Pi with networking: Pi 3, Pi 4, Pi 5 or Zero 2 W. The code is identical on all of them.
  • Raspberry Pi OS Bookworm (Lite is fine), with SSH enabled and network access.
  • One LED with a 330 ohm resistor, or a relay module, on GPIO17 (physical pin 11) and GND (pin 9). Without it, the code still runs and just logs the commands.
  • Optional: a DHT22 or DS18B20 sensor. We start with the built-in CPU temperature so nothing extra is required.
  • A second machine with mosquitto_sub and mosquitto_pub for testing (install mosquitto-clients on Linux, or use Homebrew on macOS).

Comparing boards before buying? Our Raspberry Pi vs ESP32 breakdown explains when a full Linux computer beats a microcontroller, and when it is overkill.

Step 1: Prepare Python on the Raspberry Pi

Bookworm protects the system Python, so pip install outside a virtual environment fails with an "externally managed environment" error. That is a good thing: a virtual environment keeps your project dependencies isolated and reproducible. Create one in a project folder:

sudo apt update
sudo apt install -y python3-venv python3-lgpio mosquitto-clients
mkdir -p ~/pi-mqtt && cd ~/pi-mqtt
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0" gpiozero

The --system-site-packages flag lets the virtual environment see the lgpio package that apt installed, which gpiozero uses as its pin backend on current Raspberry Pi OS (and which is required on the Pi 5, where the older RPi.GPIO library does not work). See the official Raspberry Pi GPIO documentation for the pinout of your model.

Check the installed client version. This guide uses the paho-mqtt 2.x callback API, which differs from older tutorials you may find online:

python -c "import paho.mqtt; print(paho.mqtt.__version__)"

Step 2: Your First Publish and Subscribe

Before writing the full program, prove the network path works. Pick a random device ID so your topics do not collide with anyone else on the public broker. In one terminal (on your laptop or the Pi), subscribe:

mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-laptop-7f3a -v -t "connectmq/demo/pi-7f3a/#"

In a second terminal, publish a test message:

mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-pi-7f3a -t "connectmq/demo/pi-7f3a/hello" -m "hello from the pi"

The subscriber prints the topic and message immediately. Every client needs a non-empty, unique client ID; if two clients share an ID, the broker disconnects the older one and you will see a confusing connect-disconnect loop. The free public MQTT broker is readable by anyone who guesses your topic, so never send passwords, personal data or anything you would not post publicly.

Step 3: The Complete Python MQTT Client

Save the following as ~/pi-mqtt/pi_mqtt.py. It reads all settings from environment variables, so the same file works for the public broker today and for a private broker later.

#!/usr/bin/env python3
"""Raspberry Pi MQTT client: telemetry, GPIO control and online status."""
import json
import os
import signal
import ssl
import threading
import time
import uuid

import paho.mqtt.client as mqtt

try:
    from gpiozero import LED
    led = LED(int(os.getenv("LED_PIN", "17")))
except Exception as exc:  # no GPIO (e.g. testing on a laptop)
    print(f"GPIO unavailable, running without LED: {exc}")
    led = None

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USE_TLS = os.getenv("MQTT_TLS", "0") == "1"
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
DEVICE_ID = os.getenv("DEVICE_ID", f"pi-{uuid.uuid4().hex[:6]}")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", DEVICE_ID)
PREFIX = os.getenv("TOPIC_PREFIX", f"connectmq/demo/{DEVICE_ID}")
INTERVAL = float(os.getenv("PUBLISH_INTERVAL", "10"))

T_TELEMETRY = f"{PREFIX}/telemetry"
T_LED_SET = f"{PREFIX}/led/set"
T_LED_STATE = f"{PREFIX}/led/state"
T_STATUS = f"{PREFIX}/status"

stop_event = threading.Event()


def read_cpu_temp() -> float:
    with open("/sys/class/thermal/thermal_zone0/temp") as f:
        return round(int(f.read().strip()) / 1000.0, 1)


def publish_led_state(client: mqtt.Client) -> None:
    state = "ON" if (led and led.is_lit) else "OFF"
    client.publish(T_LED_STATE, state, qos=1, retain=True)


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(T_STATUS, "online", qos=1, retain=True)
    client.subscribe(T_LED_SET, qos=1)
    publish_led_state(client)


def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"Disconnected ({reason_code}), paho will retry automatically")


def on_message(client, userdata, msg):
    command = msg.payload.decode("utf-8", errors="ignore").strip().upper()
    print(f"Command on {msg.topic}: {command}")
    if led is None:
        return
    if command == "ON":
        led.on()
    elif command == "OFF":
        led.off()
    elif command == "TOGGLE":
        led.toggle()
    else:
        print("Unknown command, ignoring")
        return
    publish_led_state(client)


def build_client() -> mqtt.Client:
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
    if USERNAME:
        client.username_pw_set(USERNAME, PASSWORD)
    if USE_TLS:
        client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
    client.will_set(T_STATUS, "offline", qos=1, retain=True)
    client.reconnect_delay_set(min_delay=1, max_delay=60)
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_message = on_message
    return client


def main() -> None:
    client = build_client()
    client.connect_async(HOST, PORT, keepalive=30)
    client.loop_start()

    signal.signal(signal.SIGTERM, lambda *_: stop_event.set())
    signal.signal(signal.SIGINT, lambda *_: stop_event.set())

    while not stop_event.is_set():
        if client.is_connected():
            payload = {
                "cpu_temp": read_cpu_temp(),
                "load": round(os.getloadavg()[0], 2),
                "ts": int(time.time()),
            }
            client.publish(T_TELEMETRY, json.dumps(payload), qos=0)
        stop_event.wait(INTERVAL)

    client.publish(T_STATUS, "offline", qos=1, retain=True).wait_for_publish(2)
    client.loop_stop()
    client.disconnect()


if __name__ == "__main__":
    main()

Run it with a fixed device ID so you know which topics to watch:

cd ~/pi-mqtt && source .venv/bin/activate
DEVICE_ID=pi-7f3a python pi_mqtt.py

How the code works

A few design choices are worth understanding, because they are where most Pi projects go wrong:

  • connect_async plus loop_start runs the network loop in a background thread and lets paho retry the first connection too. A plain connect() raises an exception and kills the script if the network is not up yet, which is common right after boot.
  • The last will (will_set) is stored by the broker at connect time. If the Pi loses power, the broker publishes offline for you after the keepalive expires. The clean shutdown path publishes offline explicitly.
  • Retained state topics mean a dashboard that connects later still sees the current LED state and online status immediately, without waiting for the next change.
  • Subscribing inside on_connect restores the subscription after every reconnect. Subscribing once after connect() is a classic bug that silently stops commands after the first network blip.
  • QoS 1 for commands and state, QoS 0 for telemetry is a sensible default. Losing one periodic reading is harmless; losing an OFF command is not. Our guide to MQTT QoS levels explains the trade-offs in depth.

Step 4: Control GPIO over MQTT

With the script running, send commands from your laptop:

mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-laptop-7f3a -q 1 -t "connectmq/demo/pi-7f3a/led/set" -m "ON"
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-laptop-7f3a -q 1 -t "connectmq/demo/pi-7f3a/led/set" -m "TOGGLE"

The LED switches and the subscriber window shows the new retained state on led/state. The same pattern drives a relay for a pump, a fan or a door strike; just swap LED for gpiozero.OutputDevice and consider inverting the logic for active-low relay boards. For buttons, PWM dimming and multiple pins, continue with the dedicated Raspberry Pi GPIO over MQTT guide, and for mains-switching hardware read Raspberry Pi relay control with MQTT for wiring safety.

A practical rule for actuators: the device, not the dashboard, owns the truth. Always publish the state the hardware actually reached, and have user interfaces display that state topic rather than assuming the command worked.

Step 5: Add a Real Sensor

The CPU temperature proves the pipeline, but most projects measure the room. Swap read_cpu_temp() for a sensor reader and add the values to the payload dictionary. The table below summarises the common choices and where to go next:

Sensor Interface What it measures Detailed guide
DHT22 / AM2302 Single GPIO, timing-sensitive Temperature, humidity DHT22 with MQTT
DS18B20 1-Wire Temperature, waterproof probes DS18B20 with MQTT
Sense HAT I2C HAT Temperature, humidity, pressure, IMU Sense HAT to MQTT
HC-SR04 Two GPIOs Distance, tank level Distance sensor to MQTT

Keep sensor reads out of the MQTT callbacks. Callbacks run on the network thread, and a slow sensor read there delays keepalive handling and acknowledgements. The main loop in the script above is the right place for anything that blocks.

Use a consistent JSON shape across devices: numeric values, SI units in the key name or a separate unit field, and a Unix timestamp. Every consumer you add later, from Grafana to Node-RED, will thank you.

Step 6: Run the MQTT Script on Boot with systemd

A script started from an SSH session dies when you log out. Turn it into a systemd service so it starts at boot and restarts on failure. Create /etc/systemd/system/pi-mqtt.service:

[Unit]
Description=Raspberry Pi MQTT client
After=network-online.target
Wants=network-online.target

[Service]
User=pi
WorkingDirectory=/home/pi/pi-mqtt
EnvironmentFile=/home/pi/pi-mqtt/.env
ExecStart=/home/pi/pi-mqtt/.venv/bin/python /home/pi/pi-mqtt/pi_mqtt.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Replace pi with your username, add the user to the gpio group if needed (sudo usermod -aG gpio pi), and create the .env file:

cat > ~/pi-mqtt/.env <<'EOF'
DEVICE_ID=pi-7f3a
PUBLISH_INTERVAL=10
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now pi-mqtt
journalctl -u pi-mqtt -f

Because the script handles SIGTERM, systemctl stop pi-mqtt publishes a clean offline status before exiting. For log rotation, watchdogs and alternatives such as cron @reboot, see running a Python MQTT script on boot.

Step 7: Move from the Public Broker to a Private Broker

The public broker is perfect for learning, but it is shared, anonymous and rate-limited (for example 10 messages per second per client and 64 KB per message). For anything you depend on, you want your own credentials, encryption and isolation. You have two options:

Option Good for You are responsible for
Mosquitto on the Pi itself Offline LANs, local-only automation Updates, TLS certificates, backups, remote access
A managed MQTT broker Devices in many places, remote dashboards, teams Only your client code

Running Mosquitto locally is covered in installing Mosquitto on a Raspberry Pi. If you choose a hosted service, ConnectMQ gives you a private broker in a couple of minutes: sign up, pick a plan, create a broker, and the portal shows a generated username, password and client ID. Then edit only the .env file:

cat > ~/pi-mqtt/.env <<'EOF'
MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_TLS=1
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
DEVICE_ID=pi-7f3a
TOPIC_PREFIX=home/livingroom/pi-7f3a
EOF
sudo systemctl restart pi-mqtt

Port 8883 uses TLS with a publicly trusted Let's Encrypt certificate, so tls_set() validates it with the CA bundle that ships with Raspberry Pi OS; you do not need to copy certificate files to the Pi. On ConnectMQ, client IDs are bound to the issued credentials, so use the client ID from the portal rather than a random one. Test from your laptop with the same credentials:

mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
  -u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID-cli -v -t "home/#"

If the portal issued a single client ID, create a second set of credentials for the laptop instead of reusing the Pi's; two connections with the same ID kick each other off. The portal also includes an in-browser MQTT client and a live topic explorer, which is often the quickest way to watch your Pi's messages arrive without installing anything.

Raspberry Pi MQTT Tutorial Checklist for Reliable Devices

Most "it worked yesterday" problems come from the same short list. Before you leave a Pi in a cupboard, run through this:

  1. Unique client ID per connection. Duplicates cause endless reconnect loops.
  2. Subscriptions inside on_connect. Otherwise commands stop after the first reconnect.
  3. Last will and retained status. Dashboards must be able to tell "quiet" from "dead".
  4. TLS on port 8883 with credentials for anything outside a lab. See the full list of MQTT security best practices.
  5. systemd with Restart=on-failure and logs in journald.
  6. Buffer readings when offline if gaps matter. Paho queues QoS 1 messages while disconnected, but only in memory; buffering MQTT messages offline in Python shows a SQLite-backed approach that survives restarts.
  7. A good power supply and a quality SD card, or boot from SSD. Under-voltage causes more random disconnects than any software bug.
  8. Harden the OS: SSH keys only, automatic security updates, no default passwords. Securing a Raspberry Pi IoT device walks through it.

Troubleshooting

Connect failed: Not authorized means the username, password or client ID does not match what the broker issued. Copy them again from the portal and check for trailing spaces in .env.

ConnectionRefusedError or timeouts on 8883 usually mean a firewall or guest network blocks outbound ports. Test with nc -vz broker.connectmq.com 8883. Some corporate networks only allow 443; in that case MQTT over secure WebSocket on port 8084 may also be blocked, so test it before committing.

ssl.SSLCertVerificationError almost always means the Pi clock is wrong. TLS needs a correct date; check timedatectl and make sure NTP is enabled.

The LED does not react but messages arrive points to GPIO permissions or wiring. Run pinctrl get 17 on recent Raspberry Pi OS to see the pin state, and confirm the user is in the gpio group.

Reconnect loop every few seconds is a duplicate client ID or a second copy of the script still running. Check ps aux | grep pi_mqtt.

Where to Go Next

This Raspberry Pi MQTT tutorial gives you a solid base, and the rest of our Raspberry Pi series builds on exactly this script:

Get Started with ConnectMQ

When your Pi project outgrows the public test broker, create a private broker on ConnectMQ. The free Serverless plan needs no credit card, supports up to 25 connections and gives you TLS on port 8883, MQTT 5 and WebSocket access out of the box, which is plenty for a home lab or a first pilot. Larger fleets can move to paid Serverless or Dedicated plans without changing a line of client code. Create your free broker at app.connectmq.com and point your .env file at it. If you want the broader picture of hosting options first, read our overview of choosing a cloud MQTT broker.

Frequently Asked Questions

Which MQTT library should I use on a Raspberry Pi?

For Python, paho-mqtt 2.x is the most widely used and is what this guide uses. Node.js users can use MQTT.js, and low-code users can run Node-RED on the Pi with the same broker settings.

Can a Raspberry Pi be both an MQTT broker and a client?

Yes. You can run Mosquitto on the Pi and connect local scripts to localhost, but you then manage security, updates and remote access yourself. Many projects keep the Pi as a client and use a hosted broker so devices and dashboards in different locations can reach each other.

Why does my Pi keep disconnecting and reconnecting?

The most common causes are a duplicate client ID, a second copy of the script running, or an unstable power supply. Check that every connection uses a unique ID and look for under-voltage warnings with dmesg.

Is the public broker safe for my Raspberry Pi project?

It is safe for learning and demos, but any client can read public topics, so never send personal or sensitive data. Use a private broker with TLS and credentials for real deployments.

Does this code work on the Raspberry Pi 5?

Yes. The script uses gpiozero with the lgpio backend, which supports the Pi 5. Libraries that write to the old GPIO memory registers, such as RPi.GPIO, do not work on the Pi 5.

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.