Raspberry Pi

Run a Python MQTT Script on Boot with systemd on Raspberry Pi

Turn a Python MQTT client into a proper systemd service on a Raspberry Pi: start after the network and clock are ready, restart automatically, keep credentials in an environment file and shut down cleanly.

Raspberry Pi terminal showing a systemd service starting a Python client at boot, illustrating how to run python mqtt script on boot
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

The reliable way to run a Python MQTT script on boot is a systemd service: it starts your client after the network is up, restarts it when it crashes, captures its logs and stops it cleanly on shutdown. In this tutorial you will turn a paho-mqtt script into a service on a Raspberry Pi, store credentials outside the code, handle the boot-time clock problem that breaks TLS and learn the commands for day-to-day operation. The same unit file works on any modern Linux distribution.

Why systemd and Not cron or rc.local

There are several ways to start a script at boot, and they are not equal:

Method Restarts on crash Waits for network Logs Clean shutdown
@reboot in crontab No No Only if you redirect output No
/etc/rc.local No Unreliable No No
Desktop autostart No No No Only with a desktop session
systemd service Yes, configurable Yes, with ordering journald built in Yes, SIGTERM then SIGKILL

A crontab entry looks simpler, but an MQTT gateway that silently dies at 3 a.m. and never comes back is the result. systemd is already running on Raspberry Pi OS, so there is nothing to install.

Step 1: Prepare the Script

A script that runs as a service should read configuration from environment variables, reconnect on its own and exit cleanly when systemd sends SIGTERM. This example publishes a heartbeat with uptime every 30 seconds and uses a Last Will so subscribers can see when the device goes offline.

#!/usr/bin/env python3
import json
import os
import signal
import threading
import time

import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.environ["MQTT_CLIENT_ID"]            # must be set and unique
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/s8t4n6/pi1")
INTERVAL = int(os.getenv("PUBLISH_INTERVAL", "30"))

stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())
signal.signal(signal.SIGINT, lambda *_: stop.set())


def on_connect(client, userdata, flags, reason_code, properties):
    print(f"connected to {HOST}:{PORT} ({reason_code})", flush=True)
    client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)


def on_disconnect(client, userdata, flags, reason_code, properties):
    print(f"disconnected ({reason_code}), will retry", flush=True)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if USERNAME:
    client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
    client.tls_set()
client.will_set(f"{PREFIX}/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.connect_async(HOST, PORT, keepalive=30)
client.loop_start()

start = time.monotonic()
while not stop.wait(INTERVAL):
    payload = {"uptime_s": int(time.monotonic() - start), "ts": int(time.time())}
    client.publish(f"{PREFIX}/heartbeat", json.dumps(payload), qos=1)

print("stopping", flush=True)
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(3)
client.disconnect()
client.loop_stop()

connect_async() matters here. A plain connect() raises an exception if the broker is unreachable at that instant, which is common in the first seconds after boot. connect_async() hands the connection to the network loop, which retries with the backoff set by reconnect_delay_set().

Step 2: Install It in a Stable Location

Do not run services from your home directory's Downloads folder. Give the gateway its own directory, virtual environment and system user:

sudo useradd --system --home /opt/mqtt-client --shell /usr/sbin/nologin mqttclient
sudo mkdir -p /opt/mqtt-client
sudo cp heartbeat.py /opt/mqtt-client/
sudo python3 -m venv /opt/mqtt-client/.venv
sudo /opt/mqtt-client/.venv/bin/pip install "paho-mqtt>=2.0"
sudo chown -R mqttclient:mqttclient /opt/mqtt-client

If the script uses GPIO, I2C or serial ports, add the user to the matching group, for example sudo usermod -aG gpio,i2c,dialout mqttclient.

Step 3: Keep Credentials in an Environment File

sudo mkdir -p /etc/mqtt-client
sudo tee /etc/mqtt-client/env > /dev/null <<'EOF'
MQTT_HOST=broker.connectmq.com
MQTT_PORT=1883
MQTT_CLIENT_ID=pi1-heartbeat-s8t4n6
TOPIC_PREFIX=connectmq/demo/s8t4n6/pi1
PUBLISH_INTERVAL=30
EOF
sudo chmod 600 /etc/mqtt-client/env

For the first test this points at the public MQTT test broker, which needs no credentials. systemd reads the file as root before starting the process, so it can stay readable by root only.

Step 4: Write the Unit File to Run the Python MQTT Script on Boot

Create /etc/systemd/system/mqtt-client.service:

[Unit]
Description=Python MQTT client
Wants=network-online.target time-sync.target
After=network-online.target time-sync.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=mqttclient
Group=mqttclient
WorkingDirectory=/opt/mqtt-client
EnvironmentFile=/etc/mqtt-client/env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mqtt-client/.venv/bin/python /opt/mqtt-client/heartbeat.py
Restart=always
RestartSec=5
TimeoutStopSec=15
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

What each important line does:

  1. Wants/After=network-online.target delays start until the network is configured, rather than just until the network service has started.
  2. time-sync.target waits for the clock (see the TLS note below).
  3. StartLimitIntervalSec=0 disables the start rate limit, so systemd keeps retrying forever instead of giving up after a few fast failures.
  4. Restart=always and RestartSec=5 restart the script five seconds after any exit, including crashes and clean exits.
  5. PYTHONUNBUFFERED=1 makes print() output appear in the journal immediately.
  6. The Protect* options sandbox the process; the secure Raspberry Pi IoT checklist explains them further.

Enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-client.service
systemctl status mqtt-client.service

Step 5: Verify and Operate

Watch the logs live, then subscribe from another machine to confirm messages arrive:

journalctl -u mqtt-client.service -f
mosquitto_sub -h broker.connectmq.com -p 1883 -i boot-viewer-s8t4n6 -t "connectmq/demo/s8t4n6/pi1/#" -v

Now reboot the Pi with sudo reboot. The subscriber sees offline (from the clean shutdown), then online and heartbeats once the Pi is back. To test crash recovery, kill the process with sudo systemctl kill -s SIGKILL mqtt-client.service and watch systemd restart it after five seconds.

Daily commands you will use:

Task Command
Restart after changing code or env sudo systemctl restart mqtt-client
Stop temporarily sudo systemctl stop mqtt-client
Disable at boot sudo systemctl disable mqtt-client
Logs since last boot journalctl -u mqtt-client -b
Show effective unit systemctl cat mqtt-client

The TLS and Clock Pitfall

Most Raspberry Pi models have no battery-backed real-time clock fitted (the Pi 5 has an RTC but needs a battery to keep time while unpowered). At boot the clock starts from the last saved time until NTP syncs. If it is far off, TLS certificate validation fails because the certificate appears not yet valid or expired, and your script logs repeated connection errors on port 8883.

The time-sync.target ordering only helps if a service actually waits for synchronisation. Enable the one that does:

sudo systemctl enable systemd-time-wait-sync.service
timedatectl status

With that enabled, the MQTT client starts only after the clock is synchronised. Because the script also reconnects on its own, it recovers even without it, just more slowly.

Troubleshooting a Service That Will Not Stay Up

When a service misbehaves, systemctl status and the journal almost always contain the answer. These are the failures seen most often with Python MQTT clients:

  1. status=203/EXEC: systemd cannot execute ExecStart. The path to the venv Python is wrong, or the venv was created by a different user and is not readable. Use absolute paths and check them with ls -l.
  2. ModuleNotFoundError: paho: the unit runs the system Python instead of the venv. Point ExecStart at /opt/mqtt-client/.venv/bin/python, not python3.
  3. KeyError: 'MQTT_CLIENT_ID': the environment file is missing a variable or has a typo. Note that EnvironmentFile lines are plain KEY=value without export.
  4. PermissionError on a file or device: ProtectSystem=strict made the path read-only, or the service user lacks a group. Add the directory to ReadWritePaths= or the user to the right group.
  5. Repeated "connection refused" or "not authorised": wrong port, credentials or a duplicate client ID. Two processes with the same client ID keep disconnecting each other, so make sure a manual test run is not still active.
  6. Restarts every few seconds forever: the script exits immediately. Run the exact ExecStart command by hand as the service user with sudo -u mqttclient to see the error in your terminal.

Moving to a Private Broker

When the service works, switch to production credentials. Create a broker on ConnectMQ, then edit /etc/mqtt-client/env:

MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
TOPIC_PREFIX=site1/gateways/pi1

Restart the service and the script connects over TLS. A managed MQTT broker keeps the server side running while systemd keeps the client side running, which together cover most outages. For crashes that look like hangs rather than exits, add an application watchdog as described in the Raspberry Pi reliability tips, and buffer data during network outages with offline MQTT buffering in Python. The systemd.service manual lists every option.

New to MQTT on the Pi? Start with the Raspberry Pi MQTT tutorial, then come back here to make your client permanent.

Keep Your Pi Connected with ConnectMQ

Sign up at app.connectmq.com for the free Serverless plan, no credit card required, and paste the generated credentials into your environment file. The ConnectMQ portal lists your devices, so after the next reboot you can confirm your service came back online without logging into the Pi.

Frequently Asked Questions

Why does my service start before the network is ready?

After=network.target only waits for the network service, not a working connection. Use Wants= and After=network-online.target, and let the script reconnect on its own with connect_async().

Why do I see no output in journalctl?

Python buffers stdout when it is not a terminal. Set PYTHONUNBUFFERED=1, run Python with -u or pass flush=True to print().

Should I use a user service instead of a system service?

User services only run while that user has a session unless lingering is enabled. A system service with a dedicated User= is simpler and starts at boot without any login.

How do I run several MQTT scripts?

Create one unit file per script, or use a template unit such as mqtt-client@.service with a separate environment file per instance. Each instance needs its own unique client ID.

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.