MQTT 5 is the current version of the MQTT standard, and it fixes many of the pain points teams hit when running MQTT 3.1.1 in production. In this guide you will learn which MQTT 5 features actually change day-to-day engineering work, see them side by side with 3.1.1, and try them yourself against the public broker at broker.connectmq.com with Mosquitto CLI tools and Python.
If you are new to the protocol, start with what an MQTT broker is and come back here.
Why MQTT 5 Exists
MQTT 3.1.1 is small, stable and everywhere. But it leaves important questions unanswered. Why was my connection refused? Why did my subscription fail? How do I spread messages from one topic over several workers? How long should the broker keep my session or an old command?
In 3.1.1 the answers were usually "the broker closed the socket" or "write it into the payload yourself". MQTT 5, standardised by OASIS in 2019, adds explicit feedback, metadata and limits to the protocol while keeping the same publish/subscribe model. Your topics, QoS levels and retained messages work exactly as before. The full specification is available from OASIS.
MQTT 3.1.1 vs MQTT 5 at a Glance
| Capability | MQTT 3.1.1 | MQTT 5 |
|---|---|---|
| Error feedback | A few CONNACK return codes, SUBACK failure flag | Reason codes on almost every acknowledgement, plus optional reason strings |
| Session lifetime | Clean session on or off | Session Expiry Interval in seconds |
| Message lifetime | Unlimited until delivered | Message Expiry Interval per message |
| Load balancing | Not in the standard | Shared subscriptions $share/{group}/{filter} |
| Bandwidth savings | Full topic in every PUBLISH | Topic aliases (integer instead of topic string) |
| Custom metadata | Only inside the payload | User properties (key/value pairs) |
| Request/response | Ad hoc conventions | Response Topic and Correlation Data |
| Flow control | Implementation specific | Receive Maximum negotiated in CONNECT/CONNACK |
| Server limits | Client has to guess | Announced in CONNACK (Maximum QoS, Retain Available and more) |
Reason Codes: Knowing Why Something Failed
The single most useful MQTT 5 change for operations is reason codes. CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK and DISCONNECT all carry a one-byte code, and the server may send a human-readable reason string with it.
| Code | Name | Typical meaning |
|---|---|---|
| 0x00 | Success | Operation accepted |
| 0x10 | No matching subscribers | QoS 1/2 publish accepted, but nobody is subscribed |
| 0x86 | Bad user name or password | Wrong credentials |
| 0x87 | Not authorized | Client is not allowed to perform the action |
| 0x89 | Server busy | Broker is protecting itself, retry later |
| 0x95 | Packet too large | Message exceeds the maximum packet size |
| 0x97 | Quota exceeded | A rate or usage limit has been reached |
Codes below 0x80 indicate success; 0x80 and above are failures. For devices in the field this is a big deal: firmware can react differently to "wrong password" (stop and alert) than to "server busy" (back off and retry). ConnectMQ returns reason codes such as 0x97 Quota exceeded and 0x89 Server busy, so your clients can tell a quota problem apart from a temporary overload.
Session Expiry and Message Expiry
Session Expiry Interval
In 3.1.1 a persistent session (clean session = false) lives until the client returns, possibly forever. MQTT 5 replaces this with two settings: Clean Start (start fresh or resume) and Session Expiry Interval (how many seconds the broker keeps the session after disconnect). A sensor that sleeps for ten minutes can ask for a 15-minute session and still get its queued QoS 1 messages when it wakes up, while abandoned sessions disappear on their own.
On ConnectMQ the session expiry is capped per plan; on the public test broker the maximum is 30 minutes.
Message Expiry Interval
A publisher can attach a lifetime in seconds to each message. If the message is still waiting in a queue or stored as retained when the interval runs out, the broker discards it. That is exactly what you want for commands like "open the valve" that are dangerous to execute an hour late.
mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -i demo-a1b2-pub \
-t "connectmq/demo/a1b2/commands/valve1" -m "open" -q 1 \
-D publish message-expiry-interval 60
Shared Subscriptions: Built-in Load Balancing
With a normal subscription every subscriber receives every message. That is a problem when one backend instance cannot keep up with thousands of devices. MQTT 5 shared subscriptions let several clients join a group; the broker delivers each message to only one member of the group.
The syntax is $share/{group}/{filter}. Publishers do not change anything: they still publish to the plain topic.
Try It: Two Workers Sharing One Topic
Open two terminals and start two consumers in the same group workers. Replace a1b2 with your own random ID, because the public area is readable by everyone.
# Terminal 1
mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i demo-a1b2-w1 \
-t '$share/workers/connectmq/demo/a1b2/jobs' -v
# Terminal 2
mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i demo-a1b2-w2 \
-t '$share/workers/connectmq/demo/a1b2/jobs' -v
Now publish six jobs from a third terminal:
for i in 1 2 3 4 5 6; do
mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -i demo-a1b2-pub \
-t "connectmq/demo/a1b2/jobs" -m "job-$i" -q 1
done
When we ran this against broker.connectmq.com, worker 1 received job-1, job-3, job-5 and worker 2 received job-2, job-4, job-6. Each job was processed once, and adding a third worker spreads the load further without touching the publishers. Single quotes around the topic matter in bash so the shell does not expand $share.
Typical uses are horizontally scaled ingestion services, command processors and any consumer you run as several container replicas.
Topic Aliases: Smaller Packets on Constrained Links
Industrial topics can be long, for example acme/izmir/machining/line1/cnc1/telemetry. Sending that string in every PUBLISH wastes bandwidth on cellular or satellite links. With a topic alias the client sends the full topic once together with an integer alias, then sends only the alias number afterwards. Each side announces how many aliases it accepts (Topic Alias Maximum) in CONNECT and CONNACK. Most client libraries handle aliases for you once you enable them.
User Properties: Metadata Without Touching the Payload
User properties are arbitrary UTF-8 key/value pairs attached to a packet. Use them for content type hints, schema versions, trace IDs or the firmware version of the sender, while the payload stays a clean sensor reading.
mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -i demo-a1b2-pub \
-t "connectmq/demo/a1b2/sensors/t1" -m '{"temperature":22.4}' \
-D publish user-property schema v2 -D publish user-property fw 1.4.0
A subscriber can print them with mosquitto_sub -V mqttv5 -F "%t %p %P".
Request/Response with Response Topic and Correlation Data
Many IoT flows are really RPC calls: "read your configuration", "reboot", "give me the last 10 values". MQTT 5 standardises this:
- The requester subscribes to its own reply topic.
- It publishes the request with a
Response Topicproperty pointing to that reply topic, andCorrelation Datathat identifies the request. - The responder answers on the response topic and copies the correlation data back.
Because the response topic is passed to subscribers, the responder does not need any hard-coded routing knowledge. You can see the properties arrive with:
mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i demo-a1b2-s \
-t "connectmq/demo/a1b2/#" -F "%t %p | response=%R props=%P"
Flow Control and Server-Announced Limits
Receive Maximum
Receive Maximum caps how many QoS 1 and QoS 2 messages may be in flight (sent but not yet acknowledged) at once. The client tells the server its limit in CONNECT and the server tells the client its limit in CONNACK. A small microcontroller can set a low value so it is never flooded, and the broker can protect itself from a client that publishes faster than it can acknowledge. For more on acknowledgements, see MQTT QoS levels explained.
Limits in CONNACK
In 3.1.1 a client discovered limits by failing. In MQTT 5 the server announces them in CONNACK properties, including Maximum QoS, Retain Available, Wildcard Subscription Available, Shared Subscription Available, Receive Maximum and Topic Alias Maximum. A well-written client reads these once and adapts, for example by downgrading to QoS 1 if QoS 2 is not offered on the current plan.
MQTT 5 in Python with Paho 2.x
The following script connects with MQTT 5, prints the limits announced in CONNACK, and runs a complete request/response round trip using message expiry, user properties, response topic and correlation data.
import time
import uuid
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
BROKER = "broker.connectmq.com"
PREFIX = "connectmq/demo/a1b2" # use your own random ID
REQ_TOPIC = f"{PREFIX}/rpc/request"
RESP_TOPIC = f"{PREFIX}/rpc/response"
def on_connect(client, userdata, flags, reason_code, properties):
print("CONNACK:", reason_code)
for name in ("MaximumQoS", "RetainAvailable", "ReceiveMaximum",
"TopicAliasMaximum", "SharedSubscriptionAvailable"):
if hasattr(properties, name):
print(f" {name} = {getattr(properties, name)}")
client.subscribe(REQ_TOPIC, qos=1)
client.subscribe(RESP_TOPIC, qos=1)
def on_message(client, userdata, msg):
props = msg.properties
if msg.topic == REQ_TOPIC:
# Responder side: answer on the requested topic, echo correlation data
reply = Properties(PacketTypes.PUBLISH)
reply.CorrelationData = props.CorrelationData
client.publish(props.ResponseTopic, b'{"uptime": 3600}', qos=1,
properties=reply)
elif msg.topic == RESP_TOPIC:
print("Response:", msg.payload.decode(),
"correlation:", props.CorrelationData.decode())
connect_props = Properties(PacketTypes.CONNECT)
connect_props.SessionExpiryInterval = 600 # keep session 10 minutes
connect_props.ReceiveMaximum = 10 # max 10 unacked inbound messages
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id="demo-a1b2-py", protocol=mqtt.MQTTv5)
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, clean_start=True, properties=connect_props)
client.loop_start()
time.sleep(2)
request = Properties(PacketTypes.PUBLISH)
request.ResponseTopic = RESP_TOPIC
request.CorrelationData = uuid.uuid4().hex.encode()
request.MessageExpiryInterval = 30
request.UserProperty = [("schema", "v1"), ("app", "blog-demo")]
client.publish(REQ_TOPIC, b'{"cmd": "status"}', qos=1, properties=request)
time.sleep(3)
client.loop_stop()
client.disconnect()
In a real system the requester and responder are different clients, but the code stays the same. For production, connect to your own broker with YOUR_USERNAME, YOUR_PASSWORD and YOUR_CLIENT_ID from the portal, ideally over TLS on port 8883 (see MQTT security best practices).
MQTT 5 Support in ConnectMQ
ConnectMQ brokers speak both MQTT 3.1.1 and MQTT 5.0 over TCP, TLS and WebSocket, so old and new devices can share the same broker. The MQTT 5 features currently implemented are:
| Feature | ConnectMQ support |
|---|---|
| Reason codes | Yes, for example 0x97 Quota exceeded and 0x89 Server busy |
| Session expiry | Yes, capped per plan (30 minutes on the public test broker) |
| Message expiry interval | Yes |
| Shared subscriptions | Yes, $share/{group}/{filter}; on the public test broker and the Serverless (Paid) plan and above |
| Topic alias | Yes |
| Response topic (request/response) | Yes, passed to subscribers |
| CONNACK limits | Maximum QoS, Retain Available, Wildcard and Shared Subscription Available, Receive Maximum |
| Retained messages | Yes |
The ConnectMQ VS Code extension shows CONNACK, SUBACK and PUBACK reason codes directly, which is handy when you are debugging why a subscription was refused.
Get Started with ConnectMQ
You can try every example in this article on broker.connectmq.com without an account. When you are ready for your own isolated broker with credentials, sign up at app.connectmq.com: the Serverless plan is free, needs no credit card and gives you a broker in minutes. Compare the options in what a managed MQTT broker is.
Frequently Asked Questions
Is MQTT 5 backward compatible with MQTT 3.1.1?
The wire format differs, so a client chooses one version when it connects. A broker that supports both, like ConnectMQ, lets MQTT 3.1.1 and MQTT 5 clients exchange messages on the same topics.
Do I need MQTT 5 for shared subscriptions?
Shared subscriptions are defined in the MQTT 5 standard. Connect with MQTT 5 (for example -V mqttv5 in Mosquitto tools) and subscribe to $share/{group}/{filter} to get load-balanced delivery.
What is the difference between session expiry and message expiry?
Session expiry controls how long the broker keeps a disconnected client's session and queued messages. Message expiry controls how long an individual message stays valid before the broker discards it.
Which MQTT 5 feature should I adopt first?
Start with reason codes, because they make failures visible at no cost. Then add session expiry for sleepy devices and shared subscriptions for backend services that need to scale.



