Raspberry Pi

Pico W MQTT TLS: Secure MicroPython Connections on Port 8883 with Certificate Verification

Encrypt and authenticate your Raspberry Pi Pico W's MQTT traffic. This guide covers firmware and library versions, loading the right root certificate, setting the clock, verifying the server, measuring handshake time and fixing the common TLS errors.

Pico W MQTT TLS connection with a padlock between the Raspberry Pi Pico W and a cloud MQTT broker on port 8883
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Pico W MQTT TLS is what turns a hobby connection into one you can trust: the traffic is encrypted, and the board verifies it is really talking to your broker before it sends a password. This guide takes a working MicroPython MQTT sketch and moves it to port 8883 with full certificate verification, covering the firmware versions you need, the root certificate to load, why the clock matters, how long the handshake takes on an RP2040, and how to fix the errors you are most likely to see.

Why TLS Matters on a Microcontroller

Plain MQTT on port 1883 sends everything in clear text, including the username and password in the CONNECT packet. Anyone on the same Wi-Fi, at the ISP or on the path to the broker can read your readings and replay your credentials. TLS fixes two problems at once:

  1. Encryption. Payloads and credentials are unreadable in transit.
  2. Server authentication. The device checks the broker's certificate chain, so a fake access point or DNS hijack cannot impersonate the broker and collect credentials.

The second point is the one most tutorials skip. Turning TLS on without verifying the certificate gives you encryption against passive eavesdroppers but no protection against an active attacker. This guide does it properly. For the wider picture, see MQTT security best practices.

If you have not connected a Pico W to MQTT before, start with the plain Raspberry Pi Pico W MQTT tutorial and come back once that works.

Firmware and Library Versions

TLS support in MicroPython has changed over the last few releases, and many older examples no longer run as written.

Component What to use Why
MicroPython firmware A current release for RPI_PICO_W (1.23 or newer) Provides ssl.SSLContext with load_verify_locations and proper hostname handling
umqtt.simple The current version from micropython-lib via mip Recent versions accept an SSLContext in the ssl argument
Root certificate ISRG Root X1 in DER format The trust anchor for Let's Encrypt certificates

Older examples pass ssl=True together with an ssl_params dictionary. If you see TypeError about unexpected keyword arguments, or AttributeError: 'bool' object has no attribute 'wrap_socket', you are mixing an old example with a new library or the other way round. Update both firmware and library and use the context-based code below.

Check what you are running from the REPL:

import sys, os
print(sys.version)
print(os.uname().machine)

Step 1: Get the Right Root Certificate

ConnectMQ brokers, including the public broker at broker.connectmq.com, use certificates issued by Let's Encrypt. You can confirm the chain yourself:

openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null \
  | grep -E "s:|i:"

The output lists the server certificate, the Let's Encrypt intermediate and, at the end, an issuer of ISRG Root X1. That root is what the Pico W should trust. Trusting the root rather than the server certificate matters: Let's Encrypt certificates are renewed every few months and intermediates change from time to time, but the root stays valid for years.

Download the root in DER format, which MicroPython loads directly:

curl -o isrgrootx1.der https://letsencrypt.org/certs/isrgrootx1.der
# or convert a PEM copy you already have
openssl x509 -in isrgrootx1.pem -outform DER -out isrgrootx1.der

Copy it to the board with Thonny's file view or:

mpremote cp isrgrootx1.der :isrgrootx1.der

Step 2: Set the Clock

The Pico W has no battery-backed clock. After every power-up, its real-time clock starts at a default date in the past. Depending on the firmware build, certificate validity periods may be checked against that clock, and a device that believes it is years ago will reject a certificate issued last month as not yet valid. Even when the check is not enforced, correct time is useful for logging and timestamps, so set it via NTP right after Wi-Fi connects:

import ntptime
ntptime.host = "pool.ntp.org"
ntptime.settime()          # sets the RTC to UTC

ntptime.settime() can raise OSError when the NTP request times out, so retry it a few times instead of letting it crash the program.

Step 3: The Complete Pico W MQTT TLS Program

Save this as main.py. For the public test broker, the credentials can stay empty and any unique client ID works. For production, fill in the values generated in the portal.

# main.py - Pico W MQTT over TLS with certificate verification
import time
import gc
import ssl
import network
import machine
import ntptime
import ubinascii
from umqtt.simple import MQTTClient

WIFI_SSID = "YOUR_WIFI_SSID"
WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"

BROKER = "broker.connectmq.com"
PORT = 8883
# Public test broker: anonymous, any unique client ID.
# Production: use the values from the broker portal.
MQTT_USER = None                 # "YOUR_USERNAME"
MQTT_PASSWORD = None             # "YOUR_PASSWORD"
CLIENT_ID = "pico-tls-" + ubinascii.hexlify(machine.unique_id()).decode()  # or "YOUR_CLIENT_ID"

TOPIC = "connectmq/demo/tl5v8n/pico1/temperature"
CA_FILE = "isrgrootx1.der"

sensor = machine.ADC(4)


def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASSWORD)
        for _ in range(40):
            if wlan.isconnected():
                break
            time.sleep(0.5)
    if not wlan.isconnected():
        raise OSError("Wi-Fi failed, status %d" % wlan.status())
    print("Wi-Fi:", wlan.ifconfig()[0])


def sync_time():
    for attempt in range(5):
        try:
            ntptime.settime()
            print("Time set:", time.gmtime())
            return
        except OSError:
            time.sleep(2)
    print("Warning: NTP failed, clock may be wrong")


def make_ssl_context():
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.verify_mode = ssl.CERT_REQUIRED
    with open(CA_FILE, "rb") as f:
        ctx.load_verify_locations(cadata=f.read())
    return ctx


def connect_mqtt(ctx):
    gc.collect()
    print("Free RAM before TLS:", gc.mem_free())
    client = MQTTClient(
        CLIENT_ID, BROKER, port=PORT,
        user=MQTT_USER, password=MQTT_PASSWORD,
        keepalive=60, ssl=ctx,
    )
    start = time.ticks_ms()
    client.connect()
    print("TLS + MQTT connect took", time.ticks_diff(time.ticks_ms(), start), "ms")
    print("Free RAM after TLS:", gc.mem_free())
    return client


def read_temperature():
    v = sensor.read_u16() * 3.3 / 65535
    return round(27 - (v - 0.706) / 0.001721, 2)


def main():
    connect_wifi()
    sync_time()
    ctx = make_ssl_context()
    client = connect_mqtt(ctx)
    while True:
        payload = '{"value": %s, "unit": "C"}' % read_temperature()
        client.publish(TOPIC, payload, qos=1)
        print("Published", payload)
        time.sleep(30)           # publishing every 30 s keeps the 60 s keepalive satisfied


while True:
    try:
        main()
    except OSError as e:
        print("Error:", e, "- reconnecting in 10 s")
        time.sleep(10)

umqtt.simple wraps the socket with the context and passes the broker name as the server hostname. That is what lets mbedTLS check that the certificate was issued for broker.connectmq.com, and it also sends SNI, which many TLS servers need to pick the right certificate.

Watch the output from your computer, also over TLS:

mosquitto_sub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
  -i laptop-tl5v8n -v -t "connectmq/demo/tl5v8n/#"

Handshake Time and Memory on the RP2040

The RP2040 has no hardware crypto acceleration, so mbedTLS does all the work in software on Cortex-M0+ cores. Expect the TLS handshake to be clearly noticeable, often on the order of a second or more, compared with a near-instant plain TCP connect. The program prints the exact figure on your network, which is worth recording because it depends on the key exchange negotiated, the certificate chain length and Wi-Fi latency.

What this means in practice:

  1. Connect once, stay connected. On mains power, keep one long-lived TLS session and send many messages over it. The handshake cost is paid only on reconnect.
  2. Be careful with deep-sleep designs. A battery device that wakes, connects, publishes and sleeps pays the full handshake every cycle, which costs time and energy. Our Pico W battery MQTT guide discusses longer intervals and batching for this case.
  3. Free memory first. A TLS session needs tens of kilobytes of RAM for buffers. Call gc.collect() before connecting, avoid creating large objects before the handshake, and compare the free-memory figures printed before and after.
  4. Consider the Pico 2 W. Its RP2350 uses faster Cortex-M33 cores, which shortens the handshake and leaves more headroom. See Pico W vs Pico 2 W.

Troubleshooting Pico W TLS Errors

Error or symptom Likely cause Fix
OSError: (-9984, 'MBEDTLS_ERR_X509_CERT_VERIFY_FAILED') Wrong or missing root certificate, or wrong hostname Load ISRG Root X1, connect by hostname not IP
Verification fails only after power-up Clock not set Run ntptime.settime() before connecting
OSError: [Errno 12] ENOMEM Not enough contiguous RAM for TLS buffers gc.collect() before connect, reduce buffers and globals
TypeError on MQTTClient(... ssl_params=...) Old example with new umqtt.simple Pass an SSLContext as ssl=
Connect works on 1883 but times out on 8883 Network blocks 8883 Test from a laptop on the same network; use a different network or ask IT to allow outbound 8883
MQTTException: 5 Not authorised Check username, password and the exact client ID from the portal
Handshake takes very long Weak Wi-Fi signal or RAM pressure Check RSSI with wlan.status("rssi") and free memory

A note on CERT_NONE: setting ctx.verify_mode = ssl.CERT_NONE makes the errors disappear, which is why it shows up in many forum answers. It also removes server authentication. Use it only for a one-off diagnosis, never in deployed code.

Moving to Your Own Private Broker

The public broker accepts anonymous TLS connections on 8883, which is perfect for testing this code. For real devices, create a private broker on ConnectMQ and copy the generated credentials into the three settings at the top of the program. The broker binds client IDs to the issued credentials, so use the portal's client ID exactly. Because the certificate is from Let's Encrypt, the same isrgrootx1.der file works for both the public and your private broker, and certificate renewals on the broker side need no change on the device.

A fully managed MQTT service also blocks IP addresses after repeated failed logins and enforces per-plan quotas, which protects your broker from misbehaving or compromised clients without any work on your side. For more on running brokers in the cloud, see our cloud MQTT broker guide, and for other Raspberry Pi projects the Raspberry Pi MQTT tutorial.

Secure Your Pico W with ConnectMQ

Create a free Serverless broker at app.connectmq.com, no credit card required, copy the username, password and client ID into main.py, and your Pico W publishes over verified TLS on port 8883. The portal's live topic explorer shows the messages arriving, so you can confirm the secure path end to end.

Frequently Asked Questions

Which certificate does the Pico W need for ConnectMQ?

ISRG Root X1, the Let's Encrypt root, in DER format. Trusting the root means routine certificate renewals on the broker do not require updating the device.

Why does my Pico W reject a valid certificate?

The most common causes are a clock that was never set, so the certificate appears not yet valid, connecting by IP address instead of hostname, or loading the wrong root certificate.

How long does a TLS handshake take on the Pico W?

It is noticeably slower than on chips with crypto hardware because the RP2040 does all cryptography in software. Measure it with time.ticks_ms() around connect() as shown above, and keep connections open rather than reconnecting for every message.

Can I use client certificates on the Pico W?

MicroPython's SSLContext supports load_cert_chain for client certificates. The broker authenticates devices with the username, password and client ID issued in the portal, so a server-verified TLS connection with those credentials is the standard setup.

Related articles

Pico W vs Pico 2 W for MQTT: RP2040 vs RP2350 Performance, Memory and TLS

The Pico 2 W keeps the Pico W's footprint and Wi-Fi chip but swaps the RP2040 for the faster RP2350. This comparison looks at what that means for MQTT devices: CPU, memory, TLS, security features, software support, and which board to buy for which project.

8 min read

Raspberry Pi RFID MQTT Access Logging with the RC522 Reader

Wire an RC522 RFID reader to a Raspberry Pi, read tags in Python, check them against a local allowlist and publish every access attempt as an MQTT event that loggers, dashboards and alerting tools can consume.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.