Adafruit MiniMQTT (adafruit_minimqtt) is the MQTT client for CircuitPython. It runs on Wi-Fi boards like the ESP32-S2, ESP32-S3 and Raspberry Pi Pico W, uses the board's built-in TLS stack, and follows a callback style that will feel familiar if you have used paho-mqtt. This tutorial installs it, connects securely, publishes a reading every 10 seconds, reacts to commands and survives Wi-Fi drops.
The examples target CircuitPython 9.x with a current MiniMQTT release from the Adafruit bundle. Because CircuitPython reloads code.py each time you save, you can iterate quickly — and you will also see why a deployed device needs a real error-handling loop.
What you need
| Item | Notes |
|---|---|
| CircuitPython board with Wi-Fi | ESP32-S2/S3 Feather or QT Py, Pico W, or an AirLift board |
| CircuitPython 9.x | Flash from circuitpython.org for your exact board |
adafruit_minimqtt library | Plus its dependencies, installed by circup |
| An MQTT broker | Examples use broker.connectmq.com (8883 TLS) |
Install the library with circup, which copies the right .mpy files and dependencies to the CIRCUITPY drive:
pip install circup
circup install adafruit_minimqtt
circup list # shows installed versions, flags outdated onesYou can also copy the adafruit_minimqtt folder from the library bundle into lib/ by hand; circup is less error-prone because it matches the bundle to your CircuitPython version.
1. Secrets in settings.toml
CircuitPython reads settings.toml from the root of the drive. The two CIRCUITPY_WIFI_* keys also make the board join Wi-Fi automatically at boot.
CIRCUITPY_WIFI_SSID = "your-ssid"
CIRCUITPY_WIFI_PASSWORD = "your-wifi-password"
MQTT_BROKER = "broker.connectmq.com"
MQTT_PORT = 8883
MQTT_USER = "username"
MQTT_PASS = "password"Read them with os.getenv(). Keeping credentials here means code.py can be shared or committed without leaking anything.
2. A complete publisher and subscriber
import os, time
import microcontroller, ssl, socketpool, wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT
if not wifi.radio.connected:
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"),
os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print("Wi-Fi:", wifi.radio.ipv4_address)
DEVICE = "cpy-" + microcontroller.cpu.uid.hex()
BASE = f"testmqtt/cpy/{DEVICE}"
def on_connect(client, userdata, flags, rc):
print("connected, rc =", rc)
client.subscribe(f"{BASE}/cmd", qos=1)
def on_disconnect(client, userdata, rc):
print("disconnected, rc =", rc)
def on_message(client, topic, message):
print(f"{topic}: {message}")
if message == "ping":
client.publish(f"{BASE}/pong", str(time.monotonic()))
pool = socketpool.SocketPool(wifi.radio)
mqtt = MQTT.MQTT(
broker=os.getenv("MQTT_BROKER"),
port=os.getenv("MQTT_PORT"),
username=os.getenv("MQTT_USER"),
password=os.getenv("MQTT_PASS"),
client_id=DEVICE,
is_ssl=True,
keep_alive=60,
socket_pool=pool,
ssl_context=ssl.create_default_context(),
)
mqtt.on_connect = on_connect
mqtt.on_disconnect = on_disconnect
mqtt.on_message = on_message
mqtt.will_set(f"{BASE}/status", "offline", retain=True, qos=1)
mqtt.connect()
mqtt.publish(f"{BASE}/status", "online", retain=True, qos=1)
last = 0
while True:
mqtt.loop(timeout=1) # reads incoming packets, sends PINGREQ when due
if time.monotonic() - last >= 10:
temp = microcontroller.cpu.temperature
mqtt.publish(f"{BASE}/temperature", f"{temp:.1f}")
last = time.monotonic()What the important lines do:
socket_poolandssl_contextconnect MiniMQTT to the board's network stack.ssl.create_default_context()uses the root certificates built into the CircuitPython firmware, so a broker with a certificate from a public CA verifies without extra files.loop(timeout=1)must run often. It processes incoming messages, callson_messageand sends keep-alive pings on schedule. The timeout must not be smaller than the client'ssocket_timeout(1 second by default), or MiniMQTT raises an error.- Subscribing in
on_connectmeans every successful connection — the first one and every reconnect — restores the subscription. This matters because shared test endpoints likebroker.connectmq.comdo not keep sessions between connections. microcontroller.cpu.uidgives a stable, unique client ID. If you prefer random IDs for throwaway tests, the client ID generator creates valid ones.microcontroller.cpu.temperatureis a convenient demo value; on some boards it returnsNone. Swap in a real sensor driver (BME280, SHT4x, …) for actual readings.
Routing per topic
When a board subscribes to several topics, per-topic callbacks keep the code tidy:
def on_led(client, topic, message):
led.value = message == "on"
mqtt.add_topic_callback(f"{BASE}/led", on_led)
mqtt.subscribe(f"{BASE}/led")Messages on that topic go to on_led; anything else still reaches the general on_message.
3. Test it from the browser
Save code.py and open the serial console (Mu, Thonny or screen /dev/tty.usbmodem* 115200). You should see the IP address, then connected, rc = 0.
Now open the online MQTT client, connect to the same broker and subscribe to testmqtt/cpy/#. You will see the retained online status and a temperature every 10 seconds. Publish ping to testmqtt/cpy/<device>/cmd and a pong comes back within a second.
Watch your CircuitPython board in the browser
To test the Last Will, unplug the board: after roughly one and a half keep-alive periods, the broker publishes offline to the status topic.
4. Surviving disconnects
In development, an exception simply stops code.py and you fix it. On a device in a cupboard, an uncaught exception means it stays silent until someone power-cycles it. Wrap the loop:
def ensure_wifi():
if not wifi.radio.connected:
print("Wi-Fi reconnect")
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"),
os.getenv("CIRCUITPY_WIFI_PASSWORD"))
delay = 1
last = 0
while True:
try:
mqtt.loop(timeout=1)
if time.monotonic() - last >= 10:
mqtt.publish(f"{BASE}/temperature", f"{microcontroller.cpu.temperature:.1f}")
last = time.monotonic()
delay = 1
except (OSError, RuntimeError, ConnectionError, MQTT.MMQTTException) as e:
print("error:", e, "- retry in", delay, "s")
time.sleep(delay)
delay = min(delay * 2, 60)
try:
ensure_wifi()
mqtt.reconnect() # on_connect runs again and resubscribes
mqtt.publish(f"{BASE}/status", "online", retain=True, qos=1)
except Exception as e2:
print("reconnect failed:", e2)
if delay >= 60:
microcontroller.reset() # last resort after repeated failuresThe pattern is the same on every platform: fix the network layer first, then MQTT, back off exponentially so a fleet does not stampede a restarting broker, and keep a hard reset as the final safety net. MQTT reconnect best practices explains the reasoning in more depth.
5. TLS and certificate notes
- Public CA certificates just work. CircuitPython ships a root certificate bundle, and
ssl.create_default_context()uses it. - Self-signed or private CA? Load it explicitly:
ctx = ssl.create_default_context(); ctx.load_verify_locations(cadata=CA_PEM)whereCA_PEMis the PEM text of your root certificate. - Memory is limited. TLS needs a sizable buffer. On boards without PSRAM, connect to MQTT early in
code.pyand avoid large allocations before the handshake. - Plain TCP on 1883 works with
is_ssl=Falseandport=1883, but sends the password in clear text. Use it only on a bench network.
If the handshake fails, rule out the broker side first with a TLS connection test from your computer.
Errors you will see
| Message | Cause |
|---|---|
ConnectionError: No network with that ssid | Wrong SSID, or a 5 GHz-only network |
ConnectionError: Authentication failure | Wrong Wi-Fi password |
MMQTTException: Connection Refused - Incorrect username/password | Broker rejected the credentials (CONNACK 4) |
MMQTTException: Connection Refused - Unauthorized | Not allowed to connect (CONNACK 5) |
OSError: [Errno 116] ETIMEDOUT | Port blocked, wrong host, or broker unreachable |
MemoryError | Not enough heap for TLS or a large payload |
The CONNACK codes are the standard MQTT 3.1.1 return codes; the reason codes tool lists them all. For a sensor build that goes further — multiple sensors, JSON payloads, a dashboard — see the MQTT weather station. And if you are choosing between platforms, the MicroPython umqtt tutorial shows the same tasks with a lower-level client.
MiniMQTT gives CircuitPython boards a complete MQTT client with TLS in a few dozen lines. Put the secrets in settings.toml, subscribe in on_connect, call loop() constantly and wrap it all in a reconnect loop, and a hobby board becomes a device you can leave running for months.
Frequently asked questions
Which boards can run Adafruit MiniMQTT?
Any CircuitPython board with a network stack: boards with native Wi-Fi such as ESP32-S2, ESP32-S3 and the Raspberry Pi Pico W, and boards that use an ESP32 AirLift co-processor through the esp32spi library. The MQTT code is the same; only the socket pool setup differs.
Does MiniMQTT support MQTT 5 or QoS 2?
No. MiniMQTT speaks MQTT 3.1.1 and supports QoS 0 and QoS 1. Brokers that support MQTT 5 accept 3.1.1 clients too, so it works with them, but you cannot use MQTT 5 properties from the board.
Why are my MiniMQTT messages strings instead of bytes?
By default MiniMQTT decodes payloads as UTF-8 and passes a str to on_message. For binary payloads such as CBOR or images, create the client with use_binary_mode=True and you will receive bytes instead.

