MQTT Security

MQTT TLS 1.3: Handshakes, Cipher Suites and Performance

How TLS 1.3 changes MQTT connections: fewer round trips, a smaller set of strong cipher suites and different trade-offs for microcontrollers. Includes Python, Node.js, Mosquitto and OpenSSL examples.

MQTT TLS 1.3 illustration comparing a two round trip TLS 1.2 handshake with a single round trip TLS 1.3 handshake between a device and broker
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

MQTT TLS 1.3 shortens the handshake, removes weak cryptography and simplifies cipher suite choice, but on battery-powered devices the real costs come from somewhere else. This article explains what actually changes between TLS 1.2 and 1.3 for MQTT connections, which cipher suites to allow, how to measure what your connection negotiates, and how to configure Python, Node.js and Mosquitto clients sensibly.

Why the TLS Version Matters for MQTT

MQTT runs over a long-lived TCP connection. A device connects once, performs a TLS handshake, and then exchanges small messages for hours or days. That means the handshake cost is paid rarely for always-on devices, but very often for devices that sleep and reconnect, such as battery sensors waking every few minutes. For those devices, handshake round trips and the CPU time spent on public key operations dominate energy use, and the TLS version and cipher suite choice matter a lot.

If you are new to securing MQTT connections, start with our MQTT TLS configuration guide and come back here for the tuning details.

TLS 1.2 vs TLS 1.3 at a Glance

Aspect TLS 1.2 TLS 1.3
Full handshake round trips 2 before application data 1 before application data
Resumed handshake 1 round trip (session ID or ticket) 1 round trip (PSK), optional 0-RTT
Key exchange RSA or (EC)DHE (EC)DHE only, always forward secret
Cipher suites Dozens, many weak or obsolete Five defined, three widely used
Certificate encrypted in handshake No Yes
CBC mode, RC4, SHA-1, static RSA Allowed (depending on config) Removed

For a client on a high-latency cellular link, saving one round trip on every connect is noticeable. For a device on a local network, the saving is small compared with the cryptographic work.

Where Handshake Cost Really Comes From

On a microcontroller, three operations dominate a full handshake:

  1. Key exchange: an ECDHE computation, typically on X25519 or the P-256 curve. This happens in both TLS versions.
  2. Certificate verification: checking the signatures in the server's chain. RSA signature verification is cheap; ECDSA verification costs more on the client but ECDSA certificates are smaller.
  3. Network round trips: each one keeps the radio awake.

TLS 1.3 cuts round trips but does not remove the public key operations. The biggest win for devices that reconnect frequently is usually session resumption, which skips certificate verification and, in TLS 1.3 PSK mode with key exchange, still provides forward secrecy. Check whether your TLS stack and broker both support it before relying on it.

Hardware acceleration matters too. The ESP32 family accelerates AES, SHA and large-number arithmetic used in RSA and ECC through mbedTLS, which makes either TLS version practical on it. Smaller microcontrollers without acceleration may benefit from ChaCha20-Poly1305, which is fast in pure software.

MQTT TLS 1.3 Cipher Suites

TLS 1.3 separates the cipher suite (symmetric encryption and hash) from the key exchange and signature algorithms. The suites you will meet in practice:

  • TLS_AES_128_GCM_SHA256: mandatory to implement, fast with AES hardware, a good default.
  • TLS_AES_256_GCM_SHA384: larger key, slightly slower, often chosen for policy reasons.
  • TLS_CHACHA20_POLY1305_SHA256: excellent on CPUs without AES acceleration.

All three are authenticated encryption and considered strong. You rarely need to restrict them. The two remaining suites defined for TLS 1.3 (CCM variants) appear mostly in constrained environments.

For TLS 1.2, if you must still support it, allow only forward-secret AEAD suites:

ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-GCM-SHA256
ECDHE-ECDSA-CHACHA20-POLY1305
ECDHE-RSA-CHACHA20-POLY1305
ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384

Avoid suites with CBC, RC4, 3DES, NULL, EXPORT, or those using static RSA key exchange (names starting with AES without ECDHE).

Measuring What Your Connection Negotiates

Never assume which version a broker supports; measure it. OpenSSL can force each version and report the result:

# Negotiate the best version both sides support
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -brief </dev/null

# Force TLS 1.3
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -tls1_3 -brief </dev/null

# Force TLS 1.2
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -tls1_2 -brief </dev/null

The Protocol version and Ciphersuite lines show the outcome. Run the same tests against any broker you evaluate, and repeat them after broker or library upgrades, because supported versions can change over time.

To time handshakes from a script, this Python snippet measures connect plus handshake latency:

import socket
import ssl
import time

HOST, PORT = "broker.connectmq.com", 8883

def handshake(version: ssl.TLSVersion) -> None:
    ctx = ssl.create_default_context()
    ctx.minimum_version = version
    ctx.maximum_version = version
    start = time.perf_counter()
    try:
        with socket.create_connection((HOST, PORT), timeout=10) as sock:
            with ctx.wrap_socket(sock, server_hostname=HOST) as tls:
                elapsed = (time.perf_counter() - start) * 1000
                print(f"{tls.version()}  {tls.cipher()[0]}  {elapsed:.1f} ms")
    except ssl.SSLError as exc:
        print(f"{version.name} not negotiated: {exc.reason}")

for v in (ssl.TLSVersion.TLSv1_2, ssl.TLSVersion.TLSv1_3):
    for _ in range(3):
        handshake(v)

Run it several times; network jitter is usually larger than the difference between versions on a fast link.

Configuring Clients

The safest client policy is "allow TLS 1.2 and newer, prefer the newest", which lets the client and server pick the best common version.

Python (paho-mqtt 2.x)

import ssl
import paho.mqtt.client as mqtt

ctx = ssl.create_default_context()           # verifies chain and hostname
ctx.minimum_version = ssl.TLSVersion.TLSv1_2  # refuse anything older

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="py-tls13-c71e")
client.tls_set_context(ctx)
# client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.connect("broker.connectmq.com", 8883)
client.loop_start()
client.publish("connectmq/demo/c71e/tls", "negotiated securely", qos=1).wait_for_publish()
client.loop_stop()
client.disconnect()

Python's set_ciphers() affects only TLS 1.2 and earlier; TLS 1.3 suites are managed by OpenSSL defaults, which are already sound.

Node.js (MQTT.js v5)

const mqtt = require("mqtt");

const client = mqtt.connect("mqtts://broker.connectmq.com:8883", {
  clientId: "node-tls13-c71e",
  protocolVersion: 5,
  minVersion: "TLSv1.2", // passed through to Node's tls.connect
});

client.on("connect", () => {
  const sock = client.stream;
  console.log("TLS:", sock.getProtocol(), sock.getCipher().name);
  client.end();
});

Mosquitto as a Self-Hosted Broker

If you run your own Mosquitto broker, you control the server side:

listener 8883
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
cafile /etc/mosquitto/certs/chain.pem
tls_version tlsv1.2
ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
ciphers_tls1.3 TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384

In Mosquitto, tls_version sets the minimum accepted version on the listener. Restricting a server to TLS 1.3 only is tempting but locks out older firmware and embedded TLS stacks that still support only TLS 1.2.

Constrained Devices: Practical Advice

  1. Keep connections open. One handshake per day is cheaper than any optimisation of one handshake per minute.
  2. Use session resumption where your stack supports it for devices that must sleep.
  3. Enable hardware crypto in your SDK configuration.
  4. Check your stack's TLS 1.3 status. Recent mbedTLS versions used by ESP-IDF can be built with TLS 1.3 support as a configuration option; older SDKs offer TLS 1.2 only, which is still secure with the suites listed above.
  5. Do not hard-code a single version in firmware. Allow negotiation so devices keep working when servers change.
  6. Avoid 0-RTT for commands. Early data can be replayed, which is dangerous for actuator commands.

The ESP32 certificate pinning guide covers the certificate side of embedded TLS, and fixing MQTT TLS handshake failures helps when version negotiation goes wrong.

Security Policy Checklist

Use this list when you write the TLS policy for an MQTT project:

  1. Minimum protocol version: TLS 1.2 on clients, with TLS 1.3 allowed and preferred.
  2. TLS 1.2 suites: ECDHE key exchange with AES-GCM or ChaCha20-Poly1305 only.
  3. TLS 1.3 suites: library defaults, unless a policy requires AES-256.
  4. Certificate verification and hostname checking: always on.
  5. Reconnect strategy: long-lived connections with backoff, not connect-per-message.
  6. Resumption: enabled where the stack supports it, for sleeping devices.
  7. Review: re-run the OpenSSL version tests after every broker, library or firmware upgrade.

Keeping the policy short and written down makes audits easier and stops one-off "temporary" exceptions, such as a disabled certificate check, from reaching production.

Leave Server Tuning to a Managed Broker

On a self-hosted broker you must track protocol deprecations, cipher recommendations and certificate renewals yourself. A managed MQTT broker handles the server side for you. ConnectMQ provides TLS on port 8883 and secure WebSocket on 8084 with a Let's Encrypt certificate, so you only need the client policy above: verify the certificate, allow TLS 1.2 and newer, and let negotiation pick the best option. Use the public MQTT test broker to run the measurements in this article from your own network.

Get Started with ConnectMQ

Sign up for ConnectMQ at app.connectmq.com, pick the free Serverless plan with no credit card, and create a broker. Point the Python or Node.js examples above at it with your generated credentials and check what your connection negotiates.

Frequently Asked Questions

Is TLS 1.2 still safe for MQTT?

Yes, when restricted to ECDHE key exchange with AEAD ciphers such as AES-GCM or ChaCha20-Poly1305. Allow TLS 1.3 alongside it so clients use it when available.

Does TLS 1.3 reduce MQTT message overhead?

Not meaningfully. Per-record overhead is similar; the main gains are a shorter handshake, stronger defaults and encrypted certificates.

Which TLS 1.3 cipher suite should IoT devices use?

TLS_AES_128_GCM_SHA256 on hardware with AES acceleration, TLS_CHACHA20_POLY1305_SHA256 on CPUs without it. Both are strong.

Should I use 0-RTT early data with MQTT?

Generally no. Early data can be replayed by an attacker, and MQTT clients gain little because connections are long-lived.

Related articles

How to Rotate MQTT Credentials Without Downtime

A step-by-step dual-credential strategy for rotating MQTT usernames and passwords across a fleet: stage the new credential, switch with fallback, verify and only then revoke the old one.

7 min read

Storing MQTT Credentials Securely on Embedded Devices

A practical comparison of ways to protect MQTT usernames and passwords on ESP32 and Linux devices, from encrypted NVS and flash encryption to secure elements and TPM-backed systemd credentials.

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.