Tutorials

MicroPython MQTT on ESP32 and Pico W (umqtt)

Connect an ESP32 or Pico W to an MQTT broker with MicroPython: umqtt.simple vs umqtt.robust, Wi-Fi setup, keep-alive pings, reconnect loops and TLS caveats.

Updated · 9 min read

MicroPython MQTT on ESP32 and Pico W (umqtt)

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.simpleumqtt.robust
MQTT version3.1.13.1.1
QoS0 and 1 (publish and subscribe)0 and 1
Automatic reconnectNoYes, on socket errors
Wi-Fi reconnectNoNo
Resubscribe after reconnectNoNo (relies on a persistent session)
Keep-alive pingsManual ping()Manual ping()
TLSYes, via an ssl contextSame 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:

Install with mpremote
pip install mpremote
mpremote mip install umqtt.simple
mpremote mip install umqtt.robust

Or, once the board is on Wi-Fi, from the REPL:

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:

wifi.py
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 wlan

Both 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

main.py
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_message always receives bytes.
  • set_callback() before subscribe() — umqtt raises an assertion error otherwise.
  • check_msg() vs wait_msg() — wait_msg() blocks until something arrives, which is fine for a pure command listener but stops your sensor loop. Use check_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:

main.py — reconnect loop
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, capped

Exponential 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:

  1. It never touches Wi-Fi. If the access point is gone, it retries a socket that cannot work.
  2. It reconnects with clean_session=False and does not resubscribe. That only restores your subscriptions if the broker kept the session — a private broker usually does, but shared test endpoints such as broker.connectmq.com do 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:

TLS with certificate verification
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 get TypeError on 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 with mpremote cp ca.der :).
  • CERT_NONE is 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() before connect(), 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

ErrorMeaningFix
MQTTException: 5CONNACK "not authorized"Wrong user or ACL denies the client
MQTTException: 4Bad username or passwordCheck credentials
MQTTException: 2Identifier rejectedClient ID empty, too long or banned
OSError: [Errno 104] ECONNRESETBroker closed the socketKeep-alive missed, duplicate client ID, TLS on a plain port
OSError: -2 or -202DNS lookup failed (port-specific code)Wi-Fi not up yet or wrong host name
MemoryError during TLS connectNot enough heapgc.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.