A Raspberry Pi Zero 2 W MQTT node gives you a full Linux computer the size of a stick of gum, which makes it a great fit for sensor nodes that need Python, TLS and remote access but not a full-size board. In this guide you will set up a headless Zero 2 W, trim its power use, read a BME280 environmental sensor and publish readings plus device health over MQTT 5, with the hardening needed to run unattended for months.
Why the Zero 2 W Makes a Good Sensor Node
The Zero 2 W has a quad-core 64-bit Arm Cortex-A53, 512 MB of RAM, 2.4 GHz Wi-Fi and Bluetooth, and the standard 40-pin GPIO header. That is enough to run Python, paho-mqtt with TLS, and a couple of sensors with plenty of headroom, while drawing noticeably less power than a Pi 4 or Pi 5.
It is not a microcontroller, however. It cannot deep sleep between readings, and it boots Linux from an SD card. If your node has to run on batteries for months, an ESP32 is the better tool; our Raspberry Pi vs ESP32 comparison explains the trade-off. The Zero 2 W shines where mains or USB power is available and you value Linux tools, SSH access and Python over the last milliwatt.
Parts List
| Part | Purpose |
|---|---|
| Raspberry Pi Zero 2 W | The node itself |
| Quality 5 V 2.5 A micro USB supply | Undervoltage is the most common cause of crashes |
| A2-rated microSD card, 16 GB or more | OS and application |
| BME280 breakout (I2C) | Temperature, humidity, pressure |
| Soldered 40-pin header or solder-on wires | The Zero 2 W ships without a header |
Headless Setup with Raspberry Pi OS Lite
Use Raspberry Pi Imager to flash Raspberry Pi OS Lite (64-bit). In the advanced options set the hostname, enable SSH with a public key, enter your Wi-Fi network and country, and create your user. The Lite image has no desktop, which saves RAM, storage writes and power. Boot the Zero, wait a minute, then connect:
ssh pi@zero-node-01.local
sudo apt update && sudo apt full-upgrade -y
sudo raspi-config nonint do_i2c 0
sudo apt install -y python3-venv i2c-tools
Remember that the Zero 2 W only supports 2.4 GHz Wi-Fi. If it never appears on the network, check that your access point broadcasts a 2.4 GHz band and that the Wi-Fi country is set correctly.
Reducing Power and SD Card Wear
Add these lines to /boot/firmware/config.txt and reboot:
# Disable Bluetooth if you do not need it
dtoverlay=disable-bt
# Turn off the activity LED
dtparam=act_led_trigger=none
Then keep the system journal in RAM so routine logging does not wear the SD card:
sudo mkdir -p /etc/systemd/journald.conf.d
printf "[Journal]\nStorage=volatile\nRuntimeMaxUse=16M\n" | \
sudo tee /etc/systemd/journald.conf.d/volatile.conf
sudo systemctl restart systemd-journald
sudo systemctl disable --now bluetooth hciuart 2>/dev/null || true
One power setting is a deliberate trade-off. Wi-Fi power saving reduces consumption but can delay incoming packets enough to cause keepalive timeouts and reconnects. If you see frequent disconnects in your logs, disable it with sudo iw wlan0 set power_save off and make it permanent through NetworkManager. For a node that only publishes, leaving power saving on is often fine.
Wiring the BME280
| BME280 | Zero 2 W |
|---|---|
| VIN | 3.3 V (physical pin 1) |
| GND | GND (physical pin 6) |
| SCL | GPIO3 / SCL (physical pin 5) |
| SDA | GPIO2 / SDA (physical pin 3) |
Run i2cdetect -y 1. You should see the sensor at address 0x76 or 0x77. Note which one; the script defaults to 0x76.
Installing the Python Environment
mkdir -p ~/node && cd ~/node
python3 -m venv .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0" adafruit-circuitpython-bme280
The Raspberry Pi Zero 2 W MQTT Script
This script uses MQTT 5 so each reading can carry a message expiry interval: if a subscriber with a persistent session is offline for longer than five minutes, stale readings are dropped instead of delivered late. It also publishes a health message with CPU temperature, Wi-Fi signal and uptime, which is invaluable once the node is hidden in a cupboard. Save as node.py:
#!/usr/bin/env python3
"""Pi Zero 2 W sensor node: BME280 + health metrics over MQTT 5."""
import json
import os
import time
import uuid
import board
import paho.mqtt.client as mqtt
from adafruit_bme280 import basic as adafruit_bme280
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
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"zero2w-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/4b9e27/zero-node-01")
INTERVAL = int(os.getenv("PUBLISH_INTERVAL", "60"))
BME_ADDRESS = int(os.getenv("BME280_ADDRESS", "0x76"), 16)
i2c = board.I2C()
bme = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=BME_ADDRESS)
expiry = Properties(PacketTypes.PUBLISH)
expiry.MessageExpiryInterval = 300
def cpu_temp():
with open("/sys/class/thermal/thermal_zone0/temp") as f:
return round(int(f.read()) / 1000, 1)
def wifi_rssi():
try:
with open("/proc/net/wireless") as f:
for line in f:
if line.strip().startswith("wlan0:"):
return float(line.split()[3].rstrip("."))
except (OSError, ValueError, IndexError):
pass
return None
def uptime_s():
with open("/proc/uptime") as f:
return int(float(f.read().split()[0]))
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 = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=CLIENT_ID,
protocol=mqtt.MQTTv5,
)
client.on_connect = on_connect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=2, max_delay=120)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()
try:
while True:
now = int(time.time())
reading = {
"temperature": round(bme.temperature, 2),
"humidity": round(bme.relative_humidity, 1),
"pressure": round(bme.pressure, 1),
"ts": now,
}
health = {
"cpu_temp": cpu_temp(),
"rssi": wifi_rssi(),
"uptime": uptime_s(),
"ts": now,
}
client.publish(f"{PREFIX}/env", json.dumps(reading), qos=1, properties=expiry)
client.publish(f"{PREFIX}/health", json.dumps(health), qos=0, properties=expiry)
print(reading, health)
time.sleep(INTERVAL)
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
Test it in the foreground with python node.py, then watch from another machine:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4b9e27 -V mqttv5 \
-t 'connectmq/demo/4b9e27/#' -v
The free public MQTT broker supports MQTT 5 and is fine for this check, but everything on it is publicly readable, so use a random prefix like 4b9e27.
Running Unattended
A sensor node is only useful if it comes back by itself after a power cut or crash. Create /etc/systemd/system/sensor-node.service:
[Unit]
Description=Zero 2 W MQTT sensor node
After=network-online.target
Wants=network-online.target
[Service]
User=pi
WorkingDirectory=/home/pi/node
EnvironmentFile=/home/pi/node/node.env
ExecStart=/home/pi/node/.venv/bin/python node.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Put your connection settings in /home/pi/node/node.env and restrict it with chmod 600:
MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
MQTT_PREFIX=site1/office/zero-node-01
Enable with sudo systemctl enable --now sensor-node. For more about service files and logs, see running a Python MQTT script on boot.
Updating Nodes in the Field
Once a node is mounted on a wall, you will still want to change its code. Keep the application in a git repository, and update with git pull followed by sudo systemctl restart sensor-node over SSH. Because the connection settings live in node.env rather than in the script, the same code runs on every node and only the environment file differs. Before pushing a change to all nodes, test it on one and watch its health and status topics for a day. A node that stops publishing health messages but still shows online is usually stuck in a sensor read, which is a hint to add timeouts around hardware access.
Hardening Checklist
- Use an official or high-quality supply; run
vcgencmd get_throttledand expectthrottled=0x0. - Keep the journal in RAM and avoid writing files in the publish loop.
- Enable the hardware watchdog with
RuntimeWatchdogSec=15in/etc/systemd/system.confso a frozen kernel reboots. - Mount the BME280 on a short cable away from the board; the Zero warms the air around it.
- Publish health data, and set the last will so the status topic shows
offlinewhen the node disappears. - Use TLS and per-device credentials before the node leaves your desk.
Scaling to Many Nodes
One Zero 2 W is a project; twenty of them is a fleet. Give every node its own credentials and client ID, a predictable topic such as site1/<room>/<node>/env, and the same image. A hosted MQTT broker removes the need to run and secure your own server. With ConnectMQ, each node's client ID is bound to its credentials, the portal's devices view lists nodes by their topic hierarchy, and a threshold alert rule can warn you when a room gets too humid. The free Serverless plan allows up to 25 connections, which covers a small fleet plus a dashboard.
If you would rather use simpler sensors, the Raspberry Pi DS18B20 MQTT guide works unchanged on the Zero 2 W, and the Raspberry Pi MQTT tutorial covers the publish and subscribe fundamentals behind this node.
Get Started with ConnectMQ
Put your Zero 2 W nodes on a cloud MQTT broker they can reach from any network. Sign up for ConnectMQ for free, no credit card needed, create a broker, and paste the generated username, password and client ID into node.env. Your node connects over TLS on port 8883 with MQTT 5, and every reading shows up in the live topic explorer.
Frequently Asked Questions
Can a Raspberry Pi Zero 2 W run on batteries?
Only for short periods, because it cannot deep sleep and draws power continuously. For long battery life use a microcontroller such as an ESP32 and keep the Zero 2 W for powered locations.
Is 512 MB of RAM enough for an MQTT sensor node?
Yes. A Python script with paho-mqtt, TLS and a sensor library uses a small fraction of it on Raspberry Pi OS Lite, leaving room for a second service.
Why does my Zero 2 W keep disconnecting from MQTT?
The usual causes are a weak power supply, a poor 2.4 GHz signal or aggressive Wi-Fi power saving. Check vcgencmd get_throttled, the RSSI in the health message, and try disabling power save.
Does the Zero 2 W support MQTT 5?
Yes. MQTT 5 is a client library feature, and paho-mqtt 2.x supports it fully, including message expiry and user properties.



