Python MQTT TLS is the difference between sending your username, password and sensor data across the internet in plain text and sending it encrypted and authenticated. In this tutorial you will configure paho-mqtt to connect on port 8883 using your operating system's trusted CA certificates, keep hostname verification switched on, and fix the certificate errors that stop most people. Every example connects to broker.connectmq.com, which serves a publicly trusted Let's Encrypt certificate, so no custom CA files are needed.
What TLS Does for MQTT
MQTT itself has no encryption. On port 1883 everything, including the CONNECT packet with your credentials, travels as readable bytes. TLS wraps the TCP connection and gives you three guarantees:
- Confidentiality: nobody on the network path can read payloads or passwords.
- Integrity: nobody can modify messages in transit without detection.
- Server authentication: your client verifies it is really talking to the broker it asked for, not an impostor.
The third point is the one people accidentally disable. Encryption without verification protects you from passive eavesdroppers but not from an active attacker who presents their own certificate. The MQTT security best practices guide covers the wider picture; this article focuses on getting the client side right.
ConnectMQ exposes these ports on every broker:
| Port | Transport | Encrypted | Use from Python |
|---|---|---|---|
| 1883 | MQTT over TCP | No | Local tests only |
| 8883 | MQTT over TLS | Yes | Default for production |
| 8083 | MQTT over WebSocket | No | Rarely needed from Python |
| 8084 | MQTT over secure WebSocket, path /mqtt |
Yes | When only web ports are allowed |
Prerequisites
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "paho-mqtt>=2.0"
Check that the broker's certificate validates from your machine before writing any Python. This command shows the chain and the verification result:
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -brief </dev/null
Look for Verification: OK. If OpenSSL already fails here, the problem is your system's CA store or network, not your code.
The Minimal Python MQTT TLS Client
Save this as tls_client.py and replace k7f3q2 with your own random string:
import ssl
import time
import uuid
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
PORT = 8883
TOPIC = "connectmq/demo/k7f3q2/secure"
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connect failed: {reason_code}")
return
sock = client.socket()
print(f"Connected over {sock.version()} using {sock.cipher()[0]}")
client.subscribe(TOPIC, qos=1)
def on_message(client, userdata, msg):
print(f"{msg.topic}: {msg.payload.decode()}")
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"tls-{uuid.uuid4().hex[:8]}")
client.on_connect = on_connect
client.on_message = on_message
# Use the operating system's trusted CAs, require a valid certificate,
# and verify that it matches BROKER. This is the secure default.
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLS_CLIENT)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()
time.sleep(2)
client.publish(TOPIC, "hello over TLS", qos=1)
time.sleep(2)
client.disconnect()
client.loop_stop()
Run it and you should see something like Connected over TLSv1.3 using TLS_AES_256_GCM_SHA384, followed by your message coming back.
What tls_set() does
Called with no ca_certs argument, tls_set() builds an SSL context that loads the platform's default trusted certificates, requires the server to present a valid certificate, and checks that the certificate's name matches the host you passed to connect(). The explicit cert_reqs and tls_version arguments above are the defaults, written out so the intent is obvious to the next reader.
If you prefer full control, build the context yourself and hand it over:
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
client.tls_set_context(context)
ssl.create_default_context() enables hostname checking and certificate verification, and setting a minimum version rejects legacy protocols. Use either tls_set() or tls_set_context(), not both.
Adding Credentials for a Private Broker
TLS protects the credentials you send, so always combine the two. With a broker created in the ConnectMQ portal:
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="YOUR_CLIENT_ID")
client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.tls_set()
client.connect("broker.connectmq.com", 8883, keepalive=60)
Use the host name and client ID shown in the portal; the broker binds client IDs to the credentials it issues. Load secrets from environment variables (os.environ["MQTT_PASSWORD"]) rather than committing them to source control.
Secure WebSocket From Python
Some networks block everything except standard web ports. paho can tunnel MQTT through secure WebSocket on port 8084 with the same TLS verification:
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=f"wss-{uuid.uuid4().hex[:8]}",
transport="websockets",
)
client.ws_set_options(path="/mqtt")
client.tls_set()
client.connect("broker.connectmq.com", 8084, keepalive=60)
Everything else, callbacks, subscriptions and publishing, stays the same. The MQTT over WebSocket article explains when this transport makes sense.
Fixing Common TLS Errors
Most TLS failures come down to a handful of causes. Match your error message to this table:
| Error | Likely cause | Fix |
|---|---|---|
CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate |
Python cannot find trusted CAs | See the CA store fixes below |
Hostname mismatch, certificate is not valid for '...' |
You connected by IP address or an alias | Connect with the exact broker host name |
certificate is not yet valid or certificate has expired |
Device clock is wrong | Sync time with NTP before connecting |
WRONG_VERSION_NUMBER |
TLS client talking to plain port 1883 | Use port 8883 |
| Connection hangs, then times out | Plain client talking to TLS port 8883, or port blocked | Call tls_set(), or try port 8084 |
Fixing a missing CA store
- macOS with Python from python.org: that Python ships without access to the system keychain. Run the "Install Certificates.command" script in the Python application folder, or point paho at certifi:
pip install certifi, thenclient.tls_set(ca_certs=certifi.where()). - Slim Docker images: install the CA bundle, for example
apt-get install -y ca-certificateson Debian-based images. - Corporate networks with TLS inspection: a proxy may replace certificates with its own. Ask IT for the corporate root CA and pass it with
ca_certs=rather than disabling verification. - Embedded Linux: update the CA bundle; very old images may not include the ISRG Root X1 certificate that Let's Encrypt chains to.
Never "fix" TLS by turning verification off
You will find advice to call client.tls_insecure_set(True) or pass cert_reqs=ssl.CERT_NONE. Both make the error disappear by accepting any certificate from anyone, which silently removes server authentication. Use them only for a quick diagnostic on your own lab network, never in deployed code.
Also avoid pinning the broker's leaf certificate in your client. Let's Encrypt certificates are renewed regularly, so a pinned certificate will break on the next renewal. Trust the CA instead, which is what the default store does.
Production Checklist
Before deploying a Python client that uses TLS, confirm each item:
- Port 8883 (or 8084 with WebSocket) is used everywhere; nothing falls back to 1883.
tls_set()ortls_set_context()is called withouttls_insecure_set(True).- The client connects by host name, never by IP address.
- The device keeps accurate time, synced by NTP at boot.
- The CA bundle on the device is maintained and updated with the OS.
- Credentials come from environment variables or a secrets store.
- Reconnect logic is in place; the paho-mqtt reconnect guide shows a robust pattern.
Where to Go Next
If you are new to paho, the paho mqtt python tutorial covers publish and subscribe basics, and the aiomqtt and asyncio guide shows how to pass a tls_context to an async client. For clients in other languages, the MQTT client libraries overview compares TLS support across ecosystems. The Python ssl module documentation is the reference for every context option used here.
Secure Your MQTT Traffic with ConnectMQ
ConnectMQ is a fully managed MQTT service with TLS on 8883 and secure WebSocket on 8084 enabled out of the box, backed by an automatically managed Let's Encrypt certificate, so your Python clients verify the broker with the CA store they already have. Test on the public MQTT test broker, then move to a private cloud MQTT broker with generated credentials. Create your free broker on the Serverless plan, no credit card required.
Frequently Asked Questions
Do I need to download a CA certificate to connect with TLS?
No. The broker uses a Let's Encrypt certificate that chains to a root trusted by current operating systems, so tls_set() with the default CA store works.
What port does MQTT over TLS use?
The standard port is 8883. ConnectMQ also offers MQTT over secure WebSocket on port 8084 with the path /mqtt.
Why does python mqtt tls fail when I connect by IP address?
Hostname verification compares the certificate's name with the host you connected to. An IP address does not match the broker's name, so the check fails by design; use the host name.
Is TLS enough to secure my MQTT client?
TLS protects the connection, but you still need strong, per-device credentials and careful topic design. Combine TLS with unique credentials from the broker portal.



