If you have ever debugged an MQTT 3.1.1 deployment where a client mysteriously refuses to connect, or where a publish silently fails to reach its subscribers, you have run into MQTT 3.1.1’s biggest weakness: it tells you almost nothing about what went wrong. The handful of CONNACK return codes are vague, most other packets carry no failure indicator at all, and broker-initiated disconnections happen by simply closing the TCP socket without any explanation. For a protocol that runs on devices you cannot easily attach a debugger to, this is more painful than it sounds.
MQTT 5 substantially fixes this with a coordinated set of feedback improvements: a far richer vocabulary of reason codes, optional reason strings for humans, the ability for the server to send a DISCONNECT with an explicit reason before closing, negative acknowledgements on the publish flow, and capability advertisements in the CONNACK that let a client adapt at connection time rather than fail later. This article covers all of these together, because they are designed to be used together. It is a technical reference; the MQTT 5 overview and each individual feature have their own articles in this category.
Table of Contents
Improved feedback at a glance
| MQTT 3.1.1 | MQTT 5 |
|---|---|
| Six CONNACK return codes | More than 20 reason codes across many packets |
| No reason codes on PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK | Reason codes on all of these |
| No error explanation on broker-initiated disconnects | Server DISCONNECT with reason code and optional reason string |
| No human-readable error context | Optional reason string accompanying reason codes |
| Client cannot query broker capabilities | CONNACK carries capability advertisement properties |
| Subscriptions failed opaquely | UNSUBACK reports per-topic outcome with reason codes |
The problem: silent failures
To appreciate what MQTT 5 changed, it helps to look at the failure modes of MQTT 3.1.1 concretely. They fall into three categories.
Unhelpful CONNACK codes. MQTT 3.1.1 has exactly six CONNACK return codes: accepted, unacceptable protocol version, identifier rejected, server unavailable, bad username or password, and not authorized. Anything else fails through one of these, and the broker has no way to say more. A client refused with “not authorized” cannot tell whether its credentials were wrong, its IP was blocklisted, its ClientId pattern was disallowed, or the broker simply ran out of license slots; they all look the same.
No feedback on other packets. PUBACK, PUBREC, PUBREL, PUBCOMP, and UNSUBACK in MQTT 3.1.1 carry only a packet identifier. There is no field for “this failed, here’s why.” If a broker cannot process a publish (because the client is not authorized for that topic, or because a quota was exceeded, or because the payload was malformed), it has no clean way to tell the client; it either silently drops the message, or closes the connection, or both, leaving the client guessing.
Silent broker-initiated disconnects. In MQTT 3.1.1, only the client can send a DISCONNECT. When the broker decides to drop a client (because of a protocol error, an authorization change, an idle timeout, anything), it just closes the TCP socket. The client sees the socket go away and has no idea why. Debugging this kind of failure typically requires reading broker logs, which are often unavailable to the team operating the client.
All three failure modes have the same root cause: the protocol gave the broker no vocabulary for explaining itself. MQTT 5 adds that vocabulary, deliberately, across the whole protocol.
Reason codes
The cornerstone of the improvement is the reason code: a single byte attached to acknowledgement and control packets that says, in protocol-defined terms, what just happened. Reason codes are not error codes specifically; they cover successes too, but they make space for failures with as much specificity as the spec authors could justify.
The set of reason codes grew dramatically. Where MQTT 3.1.1 had six return codes on the CONNACK and nothing on most other packets, MQTT 5 defines more than 20 distinct reason codes, and they can appear on many packets:
- CONNACK — the original home of return codes, now with a much larger set.
- PUBACK, PUBREC, PUBREL, PUBCOMP — the publish flow can now report problems at each step.
- SUBACK, UNSUBACK — subscribe and unsubscribe now report per-topic outcomes with specific reasons.
- DISCONNECT — both directions can now include a reason code (more below).
- AUTH — the new authentication packet uses reason codes to coordinate multi-step exchanges.
The reason codes themselves span a wide range of situations. A non-exhaustive sample, to give a sense of what is now expressible:
| Code (hex) | Name | Where it appears |
|---|---|---|
| 0x00 | Success / Normal disconnection | Most ACKs and DISCONNECT |
| 0x10 | No matching subscribers | PUBACK / PUBREC |
| 0x80 | Unspecified error | Many ACKs |
| 0x81 | Malformed packet | CONNACK, DISCONNECT |
| 0x82 | Protocol error | CONNACK, DISCONNECT |
| 0x87 | Not authorized | CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT |
| 0x8F | Topic filter invalid | SUBACK, UNSUBACK, DISCONNECT |
| 0x90 | Topic name invalid | CONNACK, PUBACK, PUBREC, DISCONNECT |
| 0x95 | Packet too large | CONNACK, PUBACK, PUBREC, DISCONNECT |
| 0x97 | Quota exceeded | PUBACK, PUBREC, SUBACK, DISCONNECT |
| 0x99 | Payload format invalid | CONNACK, PUBACK, PUBREC, DISCONNECT |
| 0x9C | Use another server | CONNACK, DISCONNECT |
| 0x9F | Connection rate exceeded | CONNACK, DISCONNECT |
This is a small slice of the available codes; the full table is in the MQTT 5 specification. The point is that the broker now has specific language for specific situations, and the client can branch on the reason code to act sensibly. “Quota Exceeded” means back off and retry later. “Topic Filter Invalid” means fix the application’s subscription strings. “Use Another Server” means follow the broker’s redirection. Each gives the client an actionable next step, where MQTT 3.1.1 gave it none.
A useful convention to know: reason codes from 0x00 to 0x7F are successes (or non-error conditions); codes from 0x80 upward indicate failures. So a quick check on whether something went wrong is to look at whether the reason code’s high bit is set. Programs can branch on the specific code; humans glancing at a packet capture can tell at a glance whether a reason code is good or bad.
Reason strings
Reason codes are numeric and machine-friendly. They are not, in themselves, particularly friendly to humans reading logs at three in the morning. MQTT 5 pairs them with an optional reason string: a human-readable UTF-8 string property that travels alongside the reason code on most packets that carry one.
The specification describes reason strings as “designed for diagnostics” and explicitly not for programmatic interpretation. The reason code is what programs branch on; the reason string is for the developer or operator trying to understand context. A SUBACK might carry reason code 0x87 (Not Authorized) together with a reason string like “Client not permitted to subscribe to topic ‘factory/secret/+’,” which gives the diagnostic specificity that the bare code cannot.
Two practical points about reason strings:
Brokers can disable them. Verbose error messages can leak information that an operator does not want to expose (which topics exist, which authorization rules are in force, why exactly a credential was rejected). Most brokers let you turn reason strings off in production, or strip them to less informative defaults, while keeping them on in development. The reason code is still sent; only the human-readable detail is suppressed.
Reason strings can be different from the reason code’s description. The standard names like “Not Authorized” are fixed and well-defined. A broker’s reason string for the same code is whatever the broker chooses to send, which can be more specific to the situation (“Client ID ‘foo’ is on the deny list” rather than “Not Authorized”). Treat the reason string as the broker’s narrative, not as a canonical translation of the code.
In day-to-day operation, reason strings are one of the biggest quality-of-life wins in MQTT 5. The difference between debugging an MQTT 3.1.1 connection failure (“not authorized” — start hunting through broker logs) and an MQTT 5 connection failure (“client identifier prefix is not in the allowed list”) is the difference between speculation and a direct answer.
Server-initiated DISCONNECT
The third major change is that the broker can now send a DISCONNECT packet to the client. In MQTT 3.1.1, the DISCONNECT was unidirectional, client to broker only. When the broker wanted to terminate a client’s connection, it had to just close the socket. MQTT 5 makes the packet bidirectional and lets the broker include a reason code (and an optional reason string) explaining what is about to happen.
The list of cases where a broker can now meaningfully send a DISCONNECT is substantial. Some examples:
- Authorization changed. The broker has decided the client should no longer be connected, perhaps because its permissions were revoked.
- Protocol error. The client sent something invalid, and the broker is closing the connection rather than continuing to deal with it.
- Server moving or shutting down. The broker is going offline and wants the client to know that the disconnection is administrative, not a fault.
- Quota exceeded. The client has exceeded a per-client limit (publish rate, subscription count, payload size) and is being disconnected for it.
- Receive Maximum exceeded. The client sent more unacknowledged QoS 1/2 publishes than the broker is willing to accept in flight (covered in the flow control article).
- Use another server. The broker is suggesting the client connect elsewhere, optionally providing the new server’s address as a property.
Each of these is a reason code the broker can put in a DISCONNECT. The client receives the DISCONNECT, processes the reason, and knows exactly what to do next. A client receiving “Use Another Server” can connect to the suggested address; a client receiving “Quota Exceeded” can throttle itself before reconnecting; a client receiving “Server Shutting Down” can choose to wait before reconnecting rather than immediately retrying.
The broker is not obligated to send a DISCONNECT before closing a connection; it can still close the socket directly if it chooses, for example for security reasons where revealing the reason would be inappropriate. But it now has the option, where MQTT 3.1.1 did not.
Negative acknowledgements on the publish flow
A subtle but important consequence of the reason-code expansion: the ordinary acknowledgement packets in the QoS 1 and QoS 2 publish flows can now carry failure information. In MQTT 3.1.1, PUBACK, PUBREC, PUBREL, and PUBCOMP had only a packet identifier; they were strictly affirmative, and the broker had no way to use them to report a problem with the publish.
MQTT 5 adds reason codes to all of these, which means they can effectively serve as negative acknowledgements. A PUBACK with reason code 0x87 (Not Authorized) tells the publisher that the broker rejected the publish because the client was not allowed to publish to that topic. A PUBACK with 0x97 (Quota Exceeded) tells it that the publish was rejected because of resource limits. A PUBREC with 0x99 (Payload Format Invalid) tells the publisher that the broker rejected the QoS 2 publish because the payload format indicator did not match the actual payload.
The publisher gets specific, actionable information without the connection being closed. In MQTT 3.1.1, a publish that the broker disliked would either be silently swallowed or result in the connection being dropped; in MQTT 5, the broker can refuse a single publish on protocol grounds while keeping the connection alive for legitimate traffic.
The same pattern extends to UNSUBACK. In MQTT 3.1.1, the UNSUBACK carried only the packet identifier; the broker could not tell the client whether the unsubscribe had actually done anything. In MQTT 5, UNSUBACK carries one reason code per topic that was in the UNSUBSCRIBE, so the client learns per-topic whether the unsubscribe succeeded (and trivially that the subscription existed) or failed (and why — possibly because no such subscription existed, possibly because the client was not authorized to unsubscribe).
CONNACK capability advertisements
The fifth piece of the feedback puzzle is what the broker tells the client at connection time. In MQTT 3.1.1, the CONNACK reported only success or failure (with one of the six return codes) and the session present flag. The client had no protocol-level way to discover what the broker did or did not support: whether QoS 2 was available, whether retained messages were allowed, whether wildcards in subscriptions were permitted. It just had to try and see what failed.
MQTT 5 adds a set of properties to the CONNACK that advertise the broker’s capabilities and limits. Some are booleans (this feature is or is not supported); some are numeric limits (this is the maximum I will accept). The most important ones:
| Property | Type | Meaning |
|---|---|---|
| Retain Available | Boolean | Whether retained messages are supported |
| Maximum QoS | Number (0/1/2) | Highest QoS the client may use |
| Wildcard Subscription Available | Boolean | Whether + and # are usable |
| Subscription Identifier Available | Boolean | Whether subscription identifiers can be used |
| Shared Subscription Available | Boolean | Whether $share/ subscriptions are supported |
| Maximum Packet Size | Number | Largest packet the broker accepts |
| Server Keep Alive | Number | Keep alive the broker is enforcing for this session |
| Session Expiry Interval | Number | The session expiry interval the broker actually granted |
| Receive Maximum | Number | Maximum unacknowledged QoS 1/2 publishes the broker will accept in flight |
| Topic Alias Maximum | Number | Highest topic alias the client may use |
| Server Reference | String | Address of another server the client should use |
| Response Information | String | Common prefix the broker suggests for request/response topics |
A well-behaved client reads the CONNACK on every connect and adjusts itself to whatever the broker actually granted. If Retain Available is false, the client should not set the retain flag on publishes; if Maximum QoS is 1, the client should not subscribe at or publish above QoS 1; if Receive Maximum is 50, the client should not try to keep more than 50 unacknowledged QoS 1/2 publishes in flight at once.
This capability advertisement removes a whole class of opaque failures. A client used to find out that QoS 2 was unavailable when its publishes mysteriously did not work; it now finds out at connect time, when it can adjust before any traffic flows. Brokers also commonly let operators configure these limits, which is valuable in multi-tenant environments where the broker operator sets policy that clients have to live within.
A worked example: a publish refused under MQTT 5
To see the parts working together, trace a single failed publish through MQTT 5.
A client connects and receives a CONNACK with Maximum QoS = 1 (the broker does not support QoS 2), Retain Available = true, and a few other capability properties. The CONNACK’s reason code is 0x00 (Success). The client knows that publishing at QoS 2 will be refused, so it will not try.
The client publishes to secrets/admin/data at QoS 1, with a payload of arbitrary bytes. The broker checks authorization, decides this client is not allowed to publish to that topic, and responds with a PUBACK whose reason code is 0x87 (Not Authorized) and whose reason string property is “Client not permitted to publish to admin topics.”
In MQTT 3.1.1, this same failure mode would have produced one of two outcomes: the PUBACK would arrive with no indication of the failure (and the client would believe the publish succeeded even though no subscribers ever saw it), or the broker would close the connection with no explanation. In MQTT 5, the client receives a specific, structured indication of the problem, with a human-readable note for the developer. The connection stays open; only this particular publish was refused. The client can log the failure, fix its application, and continue.
This is the everyday improvement MQTT 5’s feedback story provides. Not a single new mechanism, but a coordinated set of changes that together make MQTT systems much more diagnosable.
Designing a client that uses the feedback well
MQTT 5 gives clients far more information than they had before, but only clients written to use that information actually benefit. A client that ignores reason codes and capability advertisements works as poorly under MQTT 5 as the equivalent client did under MQTT 3.1.1. A few patterns separate clients that take advantage of the new feedback from clients that merely tolerate it.
Branch on the reason code, not the absence of an error. In MQTT 3.1.1, the absence of an error was effectively the only positive signal a client got. In MQTT 5, every acknowledgement carries a reason code, and the right pattern is to inspect it explicitly. Treat code 0x00 (Success) as success, treat anything from 0x80 upward as a failure that needs handling, and do not assume that “the PUBACK arrived” means “the publish was accepted.”
Distinguish retry-now, retry-later, and stop entirely. Different failure reason codes call for different responses. “Quota Exceeded” (0x97) typically means back off and try again later; “Server Busy” likewise. “Not Authorized” (0x87) means the publish or subscribe will never succeed without an application or configuration change; retrying the same operation is futile and wastes traffic. “Topic Name Invalid” (0x90) and “Topic Filter Invalid” (0x8F) are bugs in the calling code; they need a code change, not a retry. A simple classification, with one bucket per response strategy, captures most of what you need.
Read capability advertisements before sending traffic. A client that subscribes at QoS 2 to a broker whose Maximum QoS is 1 will get a downgrade reflected in the SUBACK, but it will not know until then. A client that subscribes with a wildcard to a broker whose Wildcard Subscription Available is false will simply fail. Both are avoidable by reading the CONNACK on connect and adjusting the client’s behavior to whatever the broker actually offers. The advertisements are most useful precisely when you are connecting to a broker you do not fully control.
Pair reason codes with reason strings in logs, but not in code paths. When something fails, log both: the reason code is the canonical identifier (programs and dashboards branch on it), the reason string is the human context (a developer reads it to understand the specific case). Programs should never branch on the reason string, because brokers send arbitrary text there and the wording is not standardized. Programs should always branch on the reason code, because it is.
Handle “Use Another Server” (0x9C) specifically. This reason code is the broker telling the client to connect elsewhere, often paired with a Server Reference property giving the address. A client that ignores this redirection misses one of the more useful coordination mechanisms in MQTT 5; a client that respects it can transparently follow a broker cluster’s load distribution.
Treat server-initiated DISCONNECTs as information, not just disconnections. When the broker sends a DISCONNECT, the reason code tells you whether to reconnect immediately, wait, or stop. A DISCONNECT with reason “Server Shutting Down” is not an error in the client’s behavior; reconnecting in a tight loop will not help and may make things worse. A DISCONNECT with “Receive Maximum Exceeded” indicates the client violated a flow-control limit and should fix its sending behavior before reconnecting.
These patterns are not exotic. Mostly they amount to: read the protocol’s feedback, branch on the structured part, log the human-readable part. The improved feedback in MQTT 5 only becomes valuable when client code does this consistently.
How improved feedback connects to the rest of MQTT 5
The feedback mechanisms in this article underpin many other MQTT 5 features:
- Session and Message Expiry Intervals rely on the CONNACK’s Session Expiry Interval advertisement to tell the client what the broker actually granted.
- Flow Control uses Receive Maximum from the CONNACK, plus the 0x9F (Receive Maximum Exceeded) reason code on DISCONNECT when violated.
- Enhanced Authentication uses reason codes on AUTH and CONNACK to coordinate multi-step exchanges and report success or failure.
- Topic Aliases are bounded by the Topic Alias Maximum advertised in the CONNACK.
- Shared Subscriptions rely on the Shared Subscription Available capability advertisement.
The reason-code mechanism is also the connective tissue between the protocol’s many failure points and any management or observability layer running over it. A logging system that captures reason codes from every acknowledgement has, for the first time in MQTT, enough structured information to drive metrics and alerting based on the protocol’s actual behavior.
Frequently asked questions
What is a reason code in MQTT 5?
A single-byte value attached to acknowledgement and control packets that explains, in protocol-defined terms, what happened. MQTT 5 defines more than 20 distinct reason codes, covering successes (codes below 0x80) and failures (codes from 0x80 upward), and they can appear on most acknowledgement packets and on DISCONNECT.
What is a reason string?
An optional human-readable UTF-8 string property that travels alongside a reason code. It is for diagnostics, not for programs to branch on. Brokers can disable reason strings in production to avoid leaking information.
Can the broker send a DISCONNECT in MQTT 5?
Yes. MQTT 5 makes DISCONNECT bidirectional. The broker can send a DISCONNECT carrying a reason code and optional reason string before closing the connection, telling the client exactly why. In MQTT 3.1.1, the broker could only close the socket without explanation.
What is a negative acknowledgement in MQTT 5?
An acknowledgement packet (PUBACK, PUBREC, SUBACK, UNSUBACK, etc.) carrying a reason code that indicates failure. This lets the broker reject a specific operation, such as a single publish, with a specific reason, without closing the connection.
What does CONNACK 0x87 mean?
Not Authorized. The client authenticated but is not allowed to perform the requested operation. The reason string, if included, may give more specific context. Reason code 0x87 can also appear on PUBACK, PUBREC, SUBACK, UNSUBACK, and DISCONNECT in MQTT 5, with the same general meaning.
What capability advertisements does the CONNACK carry?
Properties such as Retain Available, Maximum QoS, Wildcard Subscription Available, Shared Subscription Available, Maximum Packet Size, Server Keep Alive, Session Expiry Interval, Receive Maximum, Topic Alias Maximum, and others. A client should read these on every connect and adjust its behavior to whatever the broker actually grants.
Do reason codes exist in MQTT 3.1.1?
There are six CONNACK return codes in MQTT 3.1.1, which are the conceptual ancestors of MQTT 5’s reason codes, but no equivalent on other packets and no human-readable counterpart. MQTT 5 generalizes the mechanism and applies it consistently across the protocol.
