Raspberry Pi GPIO MQTT control lets you switch a relay, LED or buzzer from anywhere and see the state of buttons and contacts in real time, without opening a single port on your network. In this tutorial you will use gpiozero and paho-mqtt to turn an output on and off from MQTT commands, publish input changes the moment they happen and report everything as retained state. You will test it with broker.connectmq.com and a command-line client, then move it to private credentials.
How the Pieces Fit
The Pi connects outbound to an MQTT broker and subscribes to a command topic. Anything that can publish MQTT (a phone app, Node-RED, a web dashboard, a script) sends ON or OFF to that topic, and the Pi switches the pin. When the pin changes or a button is pressed, the Pi publishes the new state to a separate state topic.
That separation between command and state topics is the most important design decision in this tutorial:
| Topic | Direction | Retained | Example payload |
|---|---|---|---|
.../gpio/led/set |
Controller to Pi | No | ON |
.../gpio/led/state |
Pi to everyone | Yes | ON |
.../gpio/button/state |
Pi to everyone | Yes | PRESSED |
.../status |
Pi to everyone | Yes (Last Will) | online / offline |
Commands are not retained, so a stale "ON" never fires again when the Pi reboots. States are retained, so any dashboard that connects later sees the current values immediately. A dashboard should display the state topic, not echo what it sent, because the state topic reflects what actually happened on the hardware.
Wiring
You need an LED with a 330 ohm resistor (or a relay module) and a push button.
- LED anode (long leg) through the resistor to GPIO 17 (physical pin 11), cathode to ground.
- Push button between GPIO 27 (physical pin 13) and ground. gpiozero enables the internal pull-up, so no resistor is needed.
GPIO pins are 3.3 V and can supply only a few milliamps. Drive relays, motors and anything at mains voltage through a proper driver board or relay module, never directly. The Raspberry Pi relay over MQTT tutorial covers that safely.
Install the Libraries
gpiozero is preinstalled on Raspberry Pi OS and uses the lgpio backend, which works on every model including the Raspberry Pi 5 with its RP1 I/O chip. Create a virtual environment that can see the system packages, then add paho-mqtt:
sudo apt install -y python3-gpiozero python3-lgpio
python3 -m venv --system-site-packages ~/gpio-mqtt/.venv
source ~/gpio-mqtt/.venv/bin/activate
pip install "paho-mqtt>=2.0"
python -c "import gpiozero, paho.mqtt; print('ok')"
Using --system-site-packages avoids compiling GPIO libraries with pip, which is the most common source of installation errors on newer Pi models. Your user must be in the gpio group, which the default user already is.
The Raspberry Pi GPIO MQTT Script
import os
import signal
import uuid
import paho.mqtt.client as mqtt
from gpiozero import LED, Button
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"pi-gpio-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/g3p6v2/pi1")
led = LED(17)
button = Button(27, bounce_time=0.05)
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(1, 30)
def publish_led_state():
client.publish(f"{PREFIX}/gpio/led/state", "ON" if led.is_lit else "OFF", qos=1, retain=True)
def publish_button_state():
state = "PRESSED" if button.is_pressed else "RELEASED"
client.publish(f"{PREFIX}/gpio/button/state", state, qos=1, retain=True)
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print("connect failed:", reason_code)
return
print("connected")
client.subscribe(f"{PREFIX}/gpio/+/set", qos=1)
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
publish_led_state()
publish_button_state()
def on_message(client, userdata, msg):
target = msg.topic.split("/")[-2]
command = msg.payload.decode(errors="ignore").strip().upper()
if target != "led":
return
if command == "ON":
led.on()
elif command == "OFF":
led.off()
elif command == "TOGGLE":
led.toggle()
else:
print("ignored command:", command)
return
publish_led_state()
button.when_pressed = publish_button_state
button.when_released = publish_button_state
client.on_connect = on_connect
client.on_message = on_message
client.connect_async(BROKER, PORT, keepalive=30)
client.loop_start()
try:
signal.pause()
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
led.off()
A few details are worth pointing out. gpiozero runs when_pressed callbacks in its own thread, and paho's publish() is thread-safe, so calling it from the callback is fine. The bounce_time filters mechanical switch bounce, which otherwise produces bursts of press and release events. on_connect re-subscribes and republishes states after every reconnect, so the broker always holds the current truth. Unknown commands are ignored rather than guessed at.
Test from Another Machine
Run the script on the Pi, then from your laptop open one terminal to watch everything and another to send commands:
# terminal 1: watch all topics for this Pi
mosquitto_sub -h broker.connectmq.com -p 1883 -i gpio-watch-g3p6v2 -t "connectmq/demo/g3p6v2/pi1/#" -v
# terminal 2: send commands
mosquitto_pub -h broker.connectmq.com -p 1883 -i gpio-cmd-g3p6v2 -t "connectmq/demo/g3p6v2/pi1/gpio/led/set" -m ON
mosquitto_pub -h broker.connectmq.com -p 1883 -i gpio-cmd-g3p6v2 -t "connectmq/demo/g3p6v2/pi1/gpio/led/set" -m TOGGLE
The LED switches, and terminal 1 shows the new led/state. Press the button and button/state flips between PRESSED and RELEASED. Stop the script with Ctrl+C and the status changes to offline; pull the power instead, and the broker publishes the Last Will after the keepalive times out.
The free public MQTT broker is ideal for this test, but remember that anyone can read and publish on its public topics. Do not leave a real relay controllable there.
Extending to More Pins
To control several outputs, keep a dictionary of named devices and look them up from the topic:
from gpiozero import OutputDevice
outputs = {"led": LED(17), "fan": OutputDevice(22), "pump": OutputDevice(23, active_high=False)}
In on_message, replace the single LED with outputs.get(target) and publish state for the matching name. Use names rather than pin numbers in topics: a dashboard should not break when you rewire the board. For inputs like temperature sensors, publish on a timer instead of on change; the DHT22 MQTT tutorial shows that pattern.
Fail-Safe Behaviour for Outputs
Decide in advance what an output should do when things go wrong, because the network will eventually drop. For a light, keeping the last state is fine. For a pump, heater or valve, it is usually safer to switch off if no command or heartbeat has arrived for a while. A simple pattern is a timestamp updated in on_message and a check in the main loop that turns the output off after, say, ten minutes of silence and publishes the new state once the connection returns.
Also think about startup. gpiozero sets outputs to off when the object is created, so after a reboot every output starts in a known safe state until a fresh command arrives. Keep it that way rather than restoring the previous state from a file, unless the process genuinely requires it. If it does, document the behaviour on the device and in the dashboard so nobody is surprised.
Troubleshooting
GPIO busyorlgpio.error: another process holds the pin. Stop other scripts or services using it, and check that you are not running two copies.- No module named lgpio: the venv was created without
--system-site-packages. Recreate it or installpython3-lgpio. - Commands do nothing: check the exact topic and that the Pi printed "connected". Subscribe to
#under your prefix to see what is actually published. - LED turns on after every reboot: someone published a retained message to the
settopic. Clear it by publishing an empty retained message to that topic withmosquitto_pub -r -n. - Random button events: increase
bounce_timeor shorten the wiring; long unshielded cables pick up noise.
Production Setup
For anything beyond a demo, create a private broker on ConnectMQ and start the script with MQTT_PORT=8883, MQTT_USERNAME=YOUR_USERNAME, MQTT_PASSWORD=YOUR_PASSWORD and MQTT_CLIENT_ID=YOUR_CLIENT_ID from the portal. TLS keeps commands from being read or forged in transit, and per-device credentials mean only your clients can publish to your topics. Run it as a service with the steps in running a Python MQTT script on boot, so the outputs come back under control after a power cut. A managed MQTT broker means there is nothing to install or maintain on the Pi besides your script.
If you are just getting started, the Raspberry Pi MQTT tutorial covers installation and the basics, and the gpiozero documentation lists every supported device class.
Control Your Pi from Anywhere with ConnectMQ
Create a free Serverless broker at app.connectmq.com, no credit card required. ConnectMQ's in-browser MQTT client lets you send ON and OFF commands and watch the state topics without installing anything, so you can control your Pi from a phone or laptop minutes after signing up.
Frequently Asked Questions
Can I use RPi.GPIO instead of gpiozero?
RPi.GPIO does not support the Raspberry Pi 5. gpiozero with the lgpio backend works on all current models and handles debouncing and threading for you.
Why use retained messages for state but not for commands?
Retained state lets new subscribers see the current value instantly. A retained command would be replayed every time the Pi reconnects, switching outputs unexpectedly.
Is it safe to control mains devices this way?
Only through a properly rated relay module or contactor installed by someone qualified, with TLS and private credentials on the broker. Never switch mains directly from GPIO pins.
How quickly do commands reach the Pi?
On a healthy connection, MQTT commands typically arrive in well under a second, which is plenty for lights, relays and most automation tasks.



