Raspberry Pi

Raspberry Pi Relay MQTT Control: Switch a Multi-Channel Relay Board from Anywhere

Wire a 4-channel relay board to a Raspberry Pi, control each channel from MQTT topics with Python and gpiozero, and use retained messages so every relay returns to the right state after a reboot or power cut.

Raspberry Pi wired to a four-channel relay board with indicator LEDs, controlled by cloud MQTT messages in a raspberry pi relay mqtt project
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

A Raspberry Pi relay MQTT controller lets you switch lamps, pumps, fans or contactors from a phone, a dashboard or an automation script, without opening ports on your router. In this tutorial you will wire a multi-channel relay board, write a Python service that maps MQTT topics to relay channels, and use retained messages so every relay comes back in the correct state after a reboot.

What You Need

Item Notes
Raspberry Pi (3B+, 4, 5 or Zero 2 W) Raspberry Pi OS Bookworm, network connected
2, 4 or 8 channel relay board 5 V coil with optocouplers, 3.3 V-compatible inputs
Female-to-female jumper wires One per channel plus power and ground
Python 3 with gpiozero and paho-mqtt 2.x Installed in a virtual environment
An MQTT broker broker.connectmq.com for testing, private credentials for production

A safety note before anything else: relay boards switch whatever you connect to them. If you control mains voltage, the wiring must be done inside a proper enclosure by someone qualified, with the relay rated for the load. For learning, switch a 12 V LED strip or a small DC fan.

Wiring the Relay Board

Most hobby relay modules have a header labelled VCC, GND and IN1 to IN4. Connect them like this:

Relay pin Raspberry Pi pin
VCC 5 V (physical pin 2)
GND GND (physical pin 6)
IN1 GPIO17 (physical pin 11)
IN2 GPIO27 (physical pin 13)
IN3 GPIO22 (physical pin 15)
IN4 GPIO23 (physical pin 16)

Many of these boards are active-low: pulling an input to ground energises the coil. That is why the code below uses active_high=False. If your board clicks on when you expect off, flip that flag.

Boards with a JD-VCC jumper let you power the coils from a separate 5 V supply for full isolation. Remove the jumper, feed JD-VCC from the external supply, keep VCC on the Pi's 3.3 V or 5 V as the board's documentation says, and share ground only if the manual requires it.

One more hardware detail matters for reliability. Before your script starts, GPIO pins are inputs with default pull resistors, so an active-low board may briefly switch at boot. Test what your relays do during a power cycle. If a glitch would be harmful, pick pins whose default pull matches the "off" level or add external pull resistors.

Designing the Topic Contract

A clean topic layout makes the relay controller easy to use from any client, whether that is Node-RED, Home Assistant or a script:

Topic Direction Payload Retained
connectmq/demo/a91c4e/relays/<n>/set Controller to Pi ON, OFF (or TOGGLE, not retained) Yes for ON/OFF
connectmq/demo/a91c4e/relays/<n>/state Pi to everyone ON or OFF Yes
connectmq/demo/a91c4e/relays/status Pi (and last will) online or offline Yes

Replace a91c4e with your own random string. On a shared public broker every topic is readable by everyone, so a unique prefix keeps your test traffic separate. If you need a refresher on why separate set and state topics are good practice, the Raspberry Pi GPIO over MQTT guide explains the command and state pattern in detail.

How Retained States Survive a Reboot

Retained messages are the key to a relay controller that behaves after a power cut. The broker stores the last retained message on each topic and delivers it immediately to any new subscriber. That gives you two useful behaviours:

  1. When your dashboard publishes ON to relays/1/set with the retain flag, the broker keeps it. After the Pi reboots and subscribes to relays/+/set, it receives the desired state for every channel and restores it.
  2. When the Pi publishes relays/1/state with retain, any dashboard that opens later sees the real state immediately instead of waiting for the next change.

There is one trap: never retain TOGGLE. A retained toggle would be replayed on every reconnect and flip the relay each time. The script below ignores retained toggles for that reason. For a deeper look at the mechanism, read our explainer on MQTT retained messages.

Raspberry Pi Relay MQTT Script in Python

Create a project folder and virtual environment:

sudo apt update
sudo apt install -y python3-venv python3-lgpio
mkdir -p ~/relay-mqtt && cd ~/relay-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 use the lgpio backend that gpiozero needs on the Raspberry Pi 5 and works on older models too. Save the following as relay_mqtt.py:

#!/usr/bin/env python3
"""Control a multi-channel relay board from MQTT topics."""
import os
import signal
import sys
import uuid

import paho.mqtt.client as mqtt
from gpiozero import OutputDevice

BROKER = 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.getenv("MQTT_CLIENT_ID", f"rpi-relay-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/a91c4e/relays")

RELAY_PINS = {1: 17, 2: 27, 3: 22, 4: 23}
ACTIVE_HIGH = False  # most opto-isolated hobby boards are active-low

relays = {
    n: OutputDevice(pin, active_high=ACTIVE_HIGH, initial_value=False)
    for n, pin in RELAY_PINS.items()
}


def publish_state(client, n):
    state = "ON" if relays[n].value else "OFF"
    client.publish(f"{PREFIX}/{n}/state", state, qos=1, retain=True)


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"Connection refused: {reason_code}")
        return
    print(f"Connected as {CLIENT_ID}")
    client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
    client.subscribe(f"{PREFIX}/+/set", qos=1)
    for n in relays:
        publish_state(client, n)


def on_message(client, userdata, msg):
    try:
        n = int(msg.topic.split("/")[-2])
    except ValueError:
        return
    if n not in relays:
        print(f"Ignoring unknown channel {n}")
        return

    command = msg.payload.decode(errors="ignore").strip().upper()
    if command in ("ON", "1", "TRUE"):
        relays[n].on()
    elif command in ("OFF", "0", "FALSE"):
        relays[n].off()
    elif command == "TOGGLE":
        if msg.retain:
            print(f"Ignoring retained TOGGLE on channel {n}")
            return
        relays[n].toggle()
    else:
        print(f"Unknown command {command!r} for channel {n}")
        return
    print(f"Relay {n} -> {'ON' if relays[n].value else 'OFF'}")
    publish_state(client, n)


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)
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)

if USERNAME:
    client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
    client.tls_set()


def shutdown(signum, frame):
    for relay in relays.values():
        relay.off()
    client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True)
    client.disconnect()
    sys.exit(0)


signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

client.connect(BROKER, PORT, keepalive=30)
client.loop_forever(retry_first_connection=True)

Run it with python relay_mqtt.py. The script switches every relay off on a clean shutdown; if you would rather keep loads running when the service stops, remove the loop in shutdown.

Testing from Your Laptop

Open a second terminal on any machine with the Mosquitto clients installed and watch the state topics:

mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-a91c4e \
  -t 'connectmq/demo/a91c4e/relays/#' -v

Then switch channel 1 on with a retained command and toggle channel 2:

mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-a91c4e \
  -t 'connectmq/demo/a91c4e/relays/1/set' -m 'ON' -q 1 -r

mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-a91c4e \
  -t 'connectmq/demo/a91c4e/relays/2/set' -m 'TOGGLE' -q 1

You should hear the clicks and see relays/1/state ON appear. Now reboot the Pi. When the script reconnects, channel 1 returns to ON because the retained command is still on the broker, while channel 2 starts OFF because toggles are never retained. To clear a retained command, publish an empty retained message: mosquitto_pub ... -t '.../relays/1/set' -r -n.

Run It as a Service

A relay controller must start on boot and restart after a crash. Create /etc/systemd/system/relay-mqtt.service:

[Unit]
Description=Relay MQTT controller
After=network-online.target
Wants=network-online.target

[Service]
User=pi
WorkingDirectory=/home/pi/relay-mqtt
Environment=MQTT_HOST=broker.connectmq.com
Environment=MQTT_PORT=8883
Environment=MQTT_USERNAME=YOUR_USERNAME
Environment=MQTT_PASSWORD=YOUR_PASSWORD
Environment=MQTT_CLIENT_ID=YOUR_CLIENT_ID
Environment=MQTT_PREFIX=home/garage/relays
ExecStart=/home/pi/relay-mqtt/.venv/bin/python relay_mqtt.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable it with sudo systemctl daemon-reload && sudo systemctl enable --now relay-mqtt. The full explanation of service files, logging and environment files is in running a Python MQTT script on boot. Adjust the user name if yours is not pi.

Moving from the Test Broker to Production

The public test broker is perfect for the first hour, but anyone who guesses your topic could switch your relays. For anything connected to real equipment, use a private broker with authentication and TLS. With a fully managed MQTT service like ConnectMQ, you create a broker in the portal, copy the generated username, password and client ID into the service file above, and set the port to 8883. The client ID is bound to the issued credentials, so another device cannot impersonate your controller. The script does not change at all.

The ConnectMQ portal also helps while commissioning: its in-browser MQTT client can publish ON and OFF to each channel, and the live topic explorer shows retained state and status values at a glance. If you want a hardware alternative to the Pi for the same job, compare it with the ESP32 relay over MQTT build.

Troubleshooting

  1. Relays click on at start and off when commanded on: your board is active-high; set ACTIVE_HIGH = True.
  2. LEDs on the board light but the relay does not click: the coil supply is too weak; power JD-VCC from a separate 5 V source.
  3. Connection refused: Not authorized: check username, password and client ID, or remove them when using the anonymous public broker.
  4. Relay restores an old state after reboot: a stale retained command is on the broker; clear it with an empty retained publish.
  5. GPIO busy errors: another script or service is using the same pins; stop it before starting this one.

Get Started with ConnectMQ

Ready to control real equipment? Sign up for the free Serverless plan, no credit card needed, create a broker and point your relay service at it over TLS. You get MQTT 3.1.1 and 5.0, retained messages and a portal to watch every switch event. New to the concepts? The Raspberry Pi MQTT tutorial covers the fundamentals, and our overview of a cloud MQTT broker explains when hosting beats running Mosquitto on the Pi.

Frequently Asked Questions

Why does my relay change state after the Raspberry Pi reboots?

It is replaying a retained command from the broker, which is usually what you want. If the state is wrong, publish the correct value with retain or clear the topic with an empty retained message.

Can I use QoS 1 for relay commands?

Yes, and you should. QoS 1 guarantees the command arrives at least once, and since ON and OFF are idempotent, a duplicate delivery does no harm.

How many relays can one Raspberry Pi control?

As many as you have free GPIO pins, typically up to eight on a hobby board. For more channels, use an I2C GPIO expander such as an MCP23017 and keep the same topic layout.

Is it safe to control mains devices this way?

Only with relays rated for the load, a proper enclosure and wiring done to local electrical code. Always use authenticated TLS connections for anything that switches real equipment.

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.