MicroPython ships a tiny MQTT client called umqtt that runs on an ESP32, an ESP8266 or a Raspberry Pi Pico W in a few kilobytes of RAM. It comes in two flavours: umqtt.simple, a minimal synchronous client, and umqtt.robust, which adds automatic reconnection on top. This tutorial installs both, connects over Wi-Fi, publishes and subscribes, keeps the connection alive, recovers from drops and explains what to watch out for with TLS.
The code targets MicroPython 1.22 or newer and the current umqtt.simple from micropython-lib. It runs unchanged on ESP32 boards and the Pico W; only pin numbers differ if you read sensors.
umqtt at a glance
| umqtt.simple | umqtt.robust | |
|---|---|---|
| MQTT version | 3.1.1 | 3.1.1 |
| QoS | 0 and 1 (publish and subscribe) | 0 and 1 |
| Automatic reconnect | No | Yes, on socket errors |
| Wi-Fi reconnect | No | No |
| Resubscribe after reconnect | No | No (relies on a persistent session) |
| Keep-alive pings | Manual ping() | Manual ping() |
| TLS | Yes, via an ssl context | Same as simple |
Both are synchronous: wait_msg() blocks until a message arrives, check_msg() returns immediately. There is no background thread, so your main loop is responsible for everything.
1. Install umqtt
Some firmware builds include umqtt already (the ESP8266 port usually does). On ESP32 and Pico W, install it from micropython-lib with mpremote from your computer:
pip install mpremote
mpremote mip install umqtt.simple
mpremote mip install umqtt.robustOr, once the board is on Wi-Fi, from the REPL:
import mip
mip.install("umqtt.simple")
mip.install("umqtt.robust")Check with from umqtt.simple import MQTTClient — no ImportError means you are ready.
2. Connect to Wi-Fi
The network module is the same on ESP32 and Pico W. Put this in wifi.py so other files can reuse it:
import network, time
def connect(ssid, password, timeout=20):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(ssid, password)
start = time.time()
while not wlan.isconnected():
if time.time() - start > timeout:
raise OSError("Wi-Fi timeout, status=%s" % wlan.status())
time.sleep(0.5)
print("Wi-Fi OK", wlan.ifconfig()[0])
return wlanBoth chips only support 2.4 GHz networks. If the status stays at "connecting" forever, check the band and the password before you suspect MQTT.
3. Publish and subscribe with umqtt.simple
import time, machine, ubinascii
from umqtt.simple import MQTTClient
import wifi
wifi.connect("your-ssid", "your-wifi-password")
CLIENT_ID = b"upy-" + ubinascii.hexlify(machine.unique_id())
BASE = b"testmqtt/upy/" + CLIENT_ID
KEEPALIVE = 60
def on_message(topic, msg):
print("got", topic, msg)
if msg == b"led/on":
machine.Pin("LED", machine.Pin.OUT).on() # Pico W; use your pin on ESP32
client = MQTTClient(CLIENT_ID, "broker.connectmq.com", port=1883,
user="username", password="password",
keepalive=KEEPALIVE)
client.set_last_will(BASE + b"/status", b"offline", retain=True, qos=1)
client.set_callback(on_message) # must be set before subscribe()
client.connect()
client.publish(BASE + b"/status", b"online", retain=True, qos=1)
client.subscribe(BASE + b"/cmd", qos=1)
print("connected as", CLIENT_ID)
last_pub = last_ping = time.ticks_ms()
while True:
client.check_msg() # non-blocking: runs on_message if data waits
now = time.ticks_ms()
if time.ticks_diff(now, last_pub) > 10_000:
client.publish(BASE + b"/uptime", str(time.ticks_ms() // 1000))
last_pub = now
if time.ticks_diff(now, last_ping) > KEEPALIVE * 1000 // 2:
client.ping()
last_ping = now
time.sleep_ms(50)Key points:
- Topics and payloads are bytes. Strings often work, but bytes avoid surprises, and
on_messagealways receives bytes. set_callback()beforesubscribe()— umqtt raises an assertion error otherwise.check_msg()vswait_msg()—wait_msg()blocks until something arrives, which is fine for a pure command listener but stops your sensor loop. Usecheck_msg()when you also publish.- Keep-alive is your job. With
keepalive=60, the broker drops the client after roughly 90 seconds of silence. The loop above pings every 30 seconds. See MQTT keep alive for the exact timing rules. - Unique client ID from
machine.unique_id(), so two boards running the same file do not kick each other off.
Test it
Open the online MQTT client, connect to broker.connectmq.com and subscribe to testmqtt/upy/#. You should see the retained status message and an uptime value every 10 seconds. Publish led/on to testmqtt/upy/<your-client-id>/cmd and the REPL prints got ....
Watch your board publish in the browser
4. Reconnecting properly
Wi-Fi drops, routers reboot, brokers restart. With umqtt.simple, every network failure surfaces as an OSError — typically [Errno 104] ECONNRESET, [Errno 113] ECONNABORTED or [Errno 110] ETIMEDOUT — from whichever call touched the socket. Wrap the loop, rebuild the connection and resubscribe:
def mqtt_connect():
c = MQTTClient(CLIENT_ID, "broker.connectmq.com", user="username",
password="password", keepalive=KEEPALIVE)
c.set_last_will(BASE + b"/status", b"offline", retain=True, qos=1)
c.set_callback(on_message)
c.connect()
c.publish(BASE + b"/status", b"online", retain=True, qos=1)
c.subscribe(BASE + b"/cmd", qos=1) # subscriptions are gone after a new clean session
return c
def run_loop(c):
last_pub = last_ping = time.ticks_ms()
while True:
c.check_msg()
now = time.ticks_ms()
if time.ticks_diff(now, last_pub) > 10_000:
c.publish(BASE + b"/uptime", str(time.ticks_ms() // 1000))
last_pub = now
if time.ticks_diff(now, last_ping) > KEEPALIVE * 1000 // 2:
c.ping()
last_ping = now
time.sleep_ms(50)
client = None
delay = 1
while True:
try:
wifi.connect("your-ssid", "your-wifi-password")
client = mqtt_connect()
delay = 1
run_loop(client) # only returns by raising OSError
except OSError as e:
print("connection lost:", e, "retry in", delay, "s")
try:
client.disconnect()
except Exception:
pass
time.sleep(delay)
delay = min(delay * 2, 60) # exponential backoff, cappedExponential backoff matters when you have many devices: if the broker restarts, a fleet retrying every second all at once creates a reconnect storm. The pattern is covered in MQTT reconnect best practices.
For devices that must never hang, add a hardware watchdog: wdt = machine.WDT(timeout=30000) and call wdt.feed() once per loop iteration. If the code blocks in a socket call, the board resets itself.
What about umqtt.robust?
umqtt.robust.MQTTClient has the same API. When publish(), wait_msg() or check_msg() hits an OSError, it calls reconnect(), which retries connect(False) forever with an increasing delay. Two limitations:
- It never touches Wi-Fi. If the access point is gone, it retries a socket that cannot work.
- It reconnects with
clean_session=Falseand does not resubscribe. That only restores your subscriptions if the broker kept the session — a private broker usually does, but shared test endpoints such asbroker.connectmq.comdo not keep persistent sessions. Test persistent-session behaviour against your own broker instance.
umqtt.robust is fine for a quick prototype. For anything that runs unattended, the explicit loop above is easier to reason about.
5. TLS caveats
Port 1883 sends your username and password in clear text. For anything beyond a bench test, connect on 8883 with TLS. Current umqtt.simple takes an ssl.SSLContext:
import ssl, ntptime
ntptime.settime() # certificate validity needs a real clock
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
with open("ca.der", "rb") as f:
ctx.load_verify_locations(cadata=f.read())
client = MQTTClient(CLIENT_ID, "broker.connectmq.com", port=8883,
user="username", password="password",
keepalive=KEEPALIVE, ssl=ctx)
client.connect()Things that bite people:
- Older umqtt versions used
ssl=True, ssl_params={...}instead of a context. If you getTypeErroron the constructor, check which version is on the board. - No CA store on the device. MicroPython has no built-in root certificates. Copy the root CA that signed your broker's certificate to the board (convert PEM to DER with
openssl x509 -in ca.pem -outform der -out ca.der, upload withmpremote cp ca.der :). CERT_NONEis for debugging only. It encrypts the traffic but accepts any server, including an attacker's.- Memory. A TLS handshake needs tens of kilobytes of free heap. ESP32 and Pico W manage; ESP8266 is often too tight. Call
gc.collect()beforeconnect(), and connect early before your application fills the heap. - Clock. Without
ntptime.settime()the board thinks it is 2000 and may reject a valid certificate as not yet valid.
Testing a TLS connection shows how to check the broker's certificate chain from your laptop before you debug it on the device.
Common errors
| Error | Meaning | Fix |
|---|---|---|
MQTTException: 5 | CONNACK "not authorized" | Wrong user or ACL denies the client |
MQTTException: 4 | Bad username or password | Check credentials |
MQTTException: 2 | Identifier rejected | Client ID empty, too long or banned |
OSError: [Errno 104] ECONNRESET | Broker closed the socket | Keep-alive missed, duplicate client ID, TLS on a plain port |
OSError: -2 or -202 | DNS lookup failed (port-specific code) | Wi-Fi not up yet or wrong host name |
MemoryError during TLS connect | Not enough heap | gc.collect(), connect earlier, smaller buffers |
The numbers after MQTTException are MQTT 3.1.1 CONNACK return codes; the reason codes tool explains each one. For a deeper walk-through see MicroPython MQTT errors.
umqtt is small enough to read in one sitting, and that is its strength: once you add your own keep-alive pings, a reconnect loop with backoff, resubscription and TLS with a real CA, it is a dependable client for battery-friendly sensors. Prototype against the shared endpoint, then move the same code to a private broker by changing the host and credentials.
Frequently asked questions
Should I use umqtt.simple or umqtt.robust?
umqtt.robust is a thin subclass of umqtt.simple that reconnects automatically when a socket error occurs. It does not reconnect Wi-Fi and does not resubscribe for you, so many projects use umqtt.simple with their own reconnect loop, which gives full control over Wi-Fi, subscriptions and backoff.
Does umqtt support QoS 2 or MQTT 5?
No. umqtt.simple implements MQTT 3.1.1 with QoS 0 and QoS 1 only. If you need MQTT 5 properties or QoS 2 on a microcontroller, look at a different client or a gateway that translates for the device.
Why does my MicroPython MQTT client disconnect after about a minute?
umqtt does not send keep-alive pings by itself. If you set keepalive and then publish nothing for longer than 1.5 times that value, the broker closes the connection. Call client.ping() periodically from your main loop, or set keepalive=0 if your broker allows it.


