MQTT Fundamentals

MQTT 5 Features That Matter in Practice: Reason Codes, Shared Subscriptions, Expiry and More

MQTT 5 is more than a version bump. Learn the features that change how you build IoT systems, from reason codes and expiry to shared subscriptions and request/response, with hands-on examples.

Diagram of MQTT 5 features including shared subscriptions, reason codes and message expiry flowing through a cloud MQTT 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 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.

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:

  1. The requester subscribes to its own reply topic.
  2. It publishes the request with a Response Topic property pointing to that reply topic, and Correlation Data that identifies the request.
  3. 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.

Related articles

Why Your MQTT Client Keeps Disconnecting (and How to Fix It)

Reconnect loops are one of the most common MQTT problems. Learn the five usual causes, from duplicate client IDs to keep alive timeouts and oversized packets, and follow a diagnosis flow that pinpoints yours.

8 min read

Who Invented MQTT? The History from Oil Pipelines to IoT

MQTT was invented in 1999 by Andy Stanford-Clark of IBM and Arlen Nipper of Arcom to monitor oil pipelines over satellite. Follow its path to open source, OASIS, ISO and today's managed cloud brokers.

7 min read

MQTT Maximum Message Size: Limits and Large Payload Strategies

MQTT can technically carry a 256 MB packet, but brokers, client libraries and devices impose far lower limits. Learn where the real limits are, what happens when you exceed them, and how to send large payloads.

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.