MicroPython umqtt on ESP32 gives you MQTT in a few lines of Python, with no compile step and an interactive prompt for debugging. This tutorial connects an ESP32 to broker.connectmq.com with umqtt.simple, adds automatic reconnection with umqtt.robust, handles incoming commands without blocking and explains the limits you should know before relying on these clients in production.
Why MicroPython for MQTT Devices
C++ with the Arduino core is faster and has more libraries, but MicroPython wins on iteration speed. You edit a file, copy it to the board and run it in seconds. The REPL lets you poke at Wi-Fi, sensors and MQTT interactively, which is invaluable when you are figuring out why something does not work.
The umqtt package from micropython-lib provides two modules:
umqtt.simple: a minimal MQTT 3.1.1 client. Connect, publish, subscribe, ping, last will. Nothing happens automatically.umqtt.robust: a thin subclass ofsimplethat catches socket errors inpublish(),wait_msg()and a few other calls and reconnects automatically.
Both are small enough to fit comfortably on any ESP32.
Setting Up the Board
- Flash a current MicroPython firmware for your ESP32 from the official downloads page, using
esptool.py. - Install
mpremoteon your computer:pip install mpremote. - Check whether
umqttis already present. Some firmware builds include it, others do not. Ifimport umqtt.simplefails at the REPL, install it with mip:
mpremote connect auto mip install umqtt.simple
mpremote connect auto mip install umqtt.robust
mip needs the board to be online. If it is not yet connected to Wi-Fi, run the install from a script that connects first, or copy the module files manually with mpremote cp. The MicroPython package management docs describe all the options.
Connecting to Wi-Fi
Put this in boot.py so it runs on every start:
import network
import time
WIFI_SSID = "YOUR_WIFI"
WIFI_PASS = "YOUR_WIFI_PASSWORD"
def connect_wifi(timeout=15):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASS)
start = time.time()
while not wlan.isconnected():
if time.time() - start > timeout:
raise RuntimeError("Wi-Fi connection timed out")
time.sleep(0.25)
print("Wi-Fi OK", wlan.ifconfig()[0])
return wlan
connect_wifi()
Publishing With umqtt.simple
The first script connects to the public area of broker.connectmq.com and publishes a reading every five seconds. The client ID is built from the chip's unique ID so it is never empty and never collides with another board.
import time
import ubinascii
import machine
import esp32
from umqtt.simple import MQTTClient
BROKER = "broker.connectmq.com"
PREFIX = b"connectmq/demo/2b7e95"
CLIENT_ID = b"mpy-" + ubinascii.hexlify(machine.unique_id())
client = MQTTClient(CLIENT_ID, BROKER, port=1883, keepalive=30)
client.set_last_will(PREFIX + b"/status", b"offline", retain=True, qos=1)
client.connect()
client.publish(PREFIX + b"/status", b"online", retain=True, qos=1)
print("Connected as", CLIENT_ID)
while True:
temp_c = (esp32.raw_temperature() - 32) / 1.8 # rough internal sensor reading
payload = '{"temp":%.1f,"uptime":%d}' % (temp_c, time.ticks_ms() // 1000)
client.publish(PREFIX + b"/telemetry", payload.encode())
time.sleep(5)
On ESP32 variants without raw_temperature(), replace that line with a real sensor read. Watch the messages from your computer:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-2b7e95 -t 'connectmq/demo/2b7e95/#' -v
Note that topics and payloads are bytes. umqtt accepts str in many places, but using bytes consistently avoids surprises, especially in callbacks, which always receive bytes.
Receiving Commands Without Blocking
To receive messages, register a callback and subscribe. Then you must regularly call either wait_msg(), which blocks until a message arrives, or check_msg(), which returns immediately if nothing is waiting. Use check_msg() in a loop that also does other work:
import time
import ubinascii
import machine
from umqtt.simple import MQTTClient
BROKER = "broker.connectmq.com"
PREFIX = b"connectmq/demo/2b7e95"
CLIENT_ID = b"mpy-" + ubinascii.hexlify(machine.unique_id())
led = machine.Pin(2, machine.Pin.OUT)
def on_message(topic, msg):
print("Received", topic, msg)
if topic == PREFIX + b"/led/set":
led.value(1 if msg == b"ON" else 0)
client.publish(PREFIX + b"/led/state", msg, retain=True)
client = MQTTClient(CLIENT_ID, BROKER, port=1883, keepalive=30)
client.set_callback(on_message)
client.connect()
client.subscribe(PREFIX + b"/led/set", qos=1)
last_ping = time.ticks_ms()
last_publish = time.ticks_ms()
while True:
client.check_msg() # handle any waiting message
now = time.ticks_ms()
if time.ticks_diff(now, last_publish) > 10000:
client.publish(PREFIX + b"/telemetry", b'{"alive":true}')
last_publish = now
last_ping = now
elif time.ticks_diff(now, last_ping) > 20000:
client.ping() # keep the connection alive when idle
last_ping = now
time.sleep_ms(50)
Test it:
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-2b7e95-pub -t connectmq/demo/2b7e95/led/set -m ON
Two things are easy to miss. First, umqtt.simple does not send keepalive pings on its own. If you set a keepalive and publish less often than that, you must call ping(), as the loop above does, or the broker will drop you. Second, set_callback() must be called before subscribe().
Automatic Reconnection With umqtt.robust
umqtt.simple raises OSError when the connection drops, and your script dies unless you handle it. umqtt.robust wraps the main calls and reconnects with a short delay between attempts. The import is the only change:
from umqtt.robust import MQTTClient
There is a catch. The robust client reconnects the socket, but it does not resubscribe for you, and with a clean session the broker forgets your subscriptions. A reliable pattern is to wrap the connect step and call it both at start-up and after any error you catch yourself:
def connect_and_subscribe():
client.connect(clean_session=True)
client.subscribe(PREFIX + b"/led/set", qos=1)
client.publish(PREFIX + b"/status", b"online", retain=True, qos=1)
connect_and_subscribe()
while True:
try:
client.check_msg()
# ... periodic publishing ...
except OSError as e:
print("MQTT error", e, "- reconnecting")
time.sleep(5)
try:
connect_and_subscribe()
except OSError:
pass
time.sleep_ms(50)
For truly unattended devices, add machine.WDT(timeout=30000) and feed it in the loop, so a hard hang reboots the board.
Structuring a MicroPython umqtt ESP32 Project
Once a script grows past a few dozen lines, split it the same way you would any Python project. A layout that works well on the board:
boot.pyconnects Wi-Fi and nothing else, so a bug in the application never locks you out of the REPL.config.pyholds the broker host, port, topic prefix and credentials. Keep it out of version control and copy it to each board separately.main.pyimportsconfig, creates the client and runs the main loop inside atry/exceptthat logs the exception and callsmachine.reset()after a short delay.
Two habits save a lot of debugging time. Wrap the main loop so that pressing Ctrl+C in the REPL stops it cleanly with KeyboardInterrupt instead of being caught by your generic error handler. And call gc.collect() periodically if you build payload strings in the loop; MicroPython's heap is small and fragmentation shows up as MemoryError after hours of running, not minutes.
Publishing JSON is easiest with the built-in json module: client.publish(topic, json.dumps({"temp": 22.4})). Parsing commands works the same way with json.loads(msg) inside the callback, wrapped in a try so a malformed message cannot crash the device.
Limitations of umqtt You Should Know
| Limitation | Impact | Workaround |
|---|---|---|
| MQTT 3.1.1 only | No MQTT 5 properties, reason codes or shared subscription features | Use a different client if you need MQTT 5 |
| QoS 0 and 1 only | QoS 2 is not supported | QoS 1 is enough for most telemetry and commands |
| No automatic keepalive pings | Idle connections are dropped | Call ping() periodically |
| Blocking socket calls | connect() and wait_msg() block the script |
Use check_msg(); consider an asyncio-based client for complex devices |
| No resubscribe on reconnect | Commands stop arriving after a reconnect | Resubscribe in your own reconnect function |
| No offline message queue | Messages published while disconnected are lost | Buffer readings yourself if needed |
For devices that need to do several things concurrently, community asyncio-based MQTT clients exist that handle keepalives and reconnection in the background. For simple sensors and actuators, umqtt remains the lightest choice.
Using TLS on Port 8883
For a private broker, encrypt the connection. Recent versions of umqtt.simple accept an ssl argument that takes an SSLContext; older versions use ssl=True with an ssl_params dictionary instead, so check the version you installed.
import ssl
from umqtt.simple import MQTTClient
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile="isrg-root-x1.der") # Let's Encrypt root CA
client = MQTTClient(
b"YOUR_CLIENT_ID",
"broker.connectmq.com",
port=8883,
user=b"YOUR_USERNAME",
password=b"YOUR_PASSWORD",
keepalive=30,
ssl=ctx,
)
client.connect()
Copy the ISRG Root X1 certificate to the board first with mpremote cp. TLS needs a fair amount of RAM; boards with PSRAM handle it more comfortably. Use the exact client ID, username and password generated for your broker, because ConnectMQ binds the client ID to the issued credentials.
Related Guides
If you would rather work in C++, the ESP32 PlatformIO MQTT project guide shows a well-structured Arduino setup, and the ESP32 MQTT Last Will guide explains the status pattern used in the first script. The ESP32 IoT tutorial covers the full device-to-dashboard picture, and if you are new to the protocol, start with what an MQTT broker does.
Connect Your MicroPython Devices to ConnectMQ
All examples here work against the public MQTT test broker with no sign-up. When you move beyond experiments, ConnectMQ gives you a private MQTT broker in the cloud with TLS on 8883 and an in-browser MQTT client to check what your board sends. Create one on the free Serverless plan at app.connectmq.com, no credit card required.
Frequently Asked Questions
Does umqtt support MQTT 5?
No. Both umqtt.simple and umqtt.robust implement MQTT 3.1.1. Brokers that support MQTT 5, such as ConnectMQ, still accept 3.1.1 connections.
Why does my MicroPython MQTT client disconnect after a minute?
You set a keepalive but are not sending anything within that interval. umqtt does not ping automatically, so call client.ping() regularly or publish more often than the keepalive.
What is the difference between wait_msg() and check_msg()?
wait_msg() blocks until a message arrives. check_msg() processes a waiting message if there is one and returns immediately, which suits loops that also read sensors.
Can umqtt use QoS 2?
No. It supports QoS 0 and 1, which covers most device use cases.



