Raspberry Pi

Raspberry Pi GPIO MQTT: Control Outputs and Publish Inputs with Python

Use gpiozero and paho-mqtt to switch Raspberry Pi outputs from MQTT commands and publish input changes as retained state, with a clean command/state topic design that works on every Pi model.

Raspberry Pi breadboard with an LED and push button controlled over the network, illustrating raspberry pi gpio mqtt
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

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.

  1. LED anode (long leg) through the resistor to GPIO 17 (physical pin 11), cathode to ground.
  2. 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

  1. GPIO busy or lgpio.error: another process holds the pin. Stop other scripts or services using it, and check that you are not running two copies.
  2. No module named lgpio: the venv was created without --system-site-packages. Recreate it or install python3-lgpio.
  3. Commands do nothing: check the exact topic and that the Pi printed "connected". Subscribe to # under your prefix to see what is actually published.
  4. LED turns on after every reboot: someone published a retained message to the set topic. Clear it by publishing an empty retained message to that topic with mosquitto_pub -r -n.
  5. Random button events: increase bounce_time or 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.

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.