For two decades MQTT 3.1.1 gave you one of two answers to every operation: it worked, or it didn’t. When a PUBLISH failed, you got nothing actionable back — just silence or a disconnect. When a SUBSCRIBE half-failed (one topic accepted, one rejected), you couldn’t tell which was which. When the Server kicked you off the network, you had to guess why.
MQTT 5 fixed all of that. Every acknowledgement packet now carries a Reason Code that tells you exactly what happened — success, partial success, or one of dozens of specific failure reasons. The Server can now signal “your publish was rejected because you’re not authorized to that topic” instead of just dropping the message. It can send a DISCONNECT packet explaining “Quota exceeded” before closing the connection. SUBACK returns one Reason Code per topic filter so you know exactly which subscriptions worked. This is MQTT 5 Negative Acknowledgements — the failure-signaling framework that makes MQTT viable for production systems where silent failures are unacceptable.
This article explains how MQTT 5’s negative acknowledgement system works, which packets carry Reason Codes, what the codes mean, and how to handle failures programmatically.
For broader MQTT 5 context, see our MQTT 5 New Features Overview and MQTT 5 Reason Codes Reference.
Table of Contents
What MQTT 5 Negative Acknowledgements are in one paragraph
MQTT 5 Negative Acknowledgements are the mechanism by which Servers signal failure conditions to clients (and clients to Servers) through Reason Codes carried in acknowledgement packets. Per the OASIS MQTT v5.0 standard, Reason Codes are single-byte values where 0x00 through 0x7F indicate success or informational states and 0x80 through 0xFF indicate failure. Every MQTT 5 acknowledgement packet — CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, AUTH, and DISCONNECT — carries at least one Reason Code. Optional Reason String and User Property properties provide additional human-readable and programmatic context. This is one of the most operationally important improvements MQTT 5.0 brought over MQTT 3.1.1, transforming silent failures into actionable diagnostic information.
Why MQTT 5 changed acknowledgement behavior
MQTT 3.1.1’s acknowledgement model had serious operational gaps:
Gap 1: PUBACK said nothing about failure reasons
In MQTT 3.1.1, the QoS 1 acknowledgement (PUBACK) carried only a Packet Identifier. The Server either sent it (success) or didn’t (failure → eventually timeout → disconnect). When a publish failed because:
- The client wasn’t authorized to that topic
- The payload exceeded the Server’s size limit
- The Server’s quota was exceeded
- The topic name was invalid
…you got the same response: nothing. The client had no way to distinguish a network glitch from “you’ll never be allowed to publish to this topic.”
Gap 2: SUBACK was ambiguous
MQTT 3.1.1’s SUBACK returned a single byte per topic filter: 0x00, 0x01, or 0x02 for granted QoS levels, or 0x80 for “Failure.” But 0x80 meant any failure — invalid topic filter, not authorized, Server-side error. You couldn’t distinguish them.
Gap 3: Server couldn’t explain why it disconnected
In MQTT 3.1.1, only the client sent DISCONNECT packets. When the Server needed to terminate a connection, it just closed the TCP socket. The client had no way to know why — Server overload? Authentication revoked? Network issue? Each one needs different handling.
Gap 4: No granular per-packet diagnostics
Production systems need to log specific failure modes for monitoring, alerting, and troubleshooting. MQTT 3.1.1 gave you “it succeeded” or “it didn’t” — not enough for modern observability.
MQTT 5 fixed all four gaps by adding Reason Codes to every acknowledgement packet and introducing Server-initiated DISCONNECT. The result: production-grade error handling that programmatic systems can actually use.
The Reason Code system
Per OASIS MQTT v5.0 Section 2.4, Reason Codes are single-byte values with a clean structural division:
| Range | Meaning |
|---|---|
| 0x00 – 0x7F | Success or informational (positive acknowledgement) |
| 0x80 – 0xFF | Failure or negative acknowledgement |
Bit 7 set (≥0x80) means failure. Bit 7 clear (≤0x7F) means success or informational. This is a hard rule — applications can use bit 7 alone to determine success/failure without checking specific values.
Within each range, specific values have defined meanings. The success range is small (Success = 0x00, Granted QoS 0/1/2 = 0x00/0x01/0x02, Disconnect with Will Message = 0x04, No matching subscribers = 0x10, etc.). The failure range is rich — over 30 distinct failure reason codes covering authentication, authorization, protocol errors, resource exhaustion, configuration, and more.
Common Negative Reason Codes appearing across MQTT 5 packets:
| Code | Hex | Name | Common Cause |
|---|---|---|---|
| 128 | 0x80 | Unspecified error | Server can’t determine specific reason |
| 129 | 0x81 | Malformed Packet | Packet doesn’t conform to spec |
| 130 | 0x82 | Protocol Error | Logical protocol violation |
| 131 | 0x83 | Implementation specific error | Server-side issue not in standard codes |
| 134 | 0x86 | Bad User Name or Password | Authentication failed |
| 135 | 0x87 | Not authorized | Authentication OK but not authorized |
| 136 | 0x88 | Server unavailable | Server transient unavailability |
| 137 | 0x89 | Server busy | Server overloaded |
| 138 | 0x8A | Banned | Client banned by administrative action |
| 139 | 0x8B | Server shutting down | Server graceful shutdown |
| 141 | 0x8D | Keep Alive timeout | Client missed Keep Alive |
| 142 | 0x8E | Session taken over | Same Client Identifier connected elsewhere |
| 143 | 0x8F | Topic Filter invalid | Subscription topic format wrong |
| 144 | 0x90 | Topic Name invalid | Publish topic format wrong |
| 145 | 0x91 | Packet identifier in use | Reuse of active Packet Identifier |
| 146 | 0x92 | Packet Identifier not found | Reference to non-existent Packet ID |
| 147 | 0x93 | Receive Maximum exceeded | Too many in-flight QoS messages |
| 148 | 0x94 | Topic Alias invalid | Topic Alias number out of range or unmapped |
| 149 | 0x95 | Packet too large | Packet exceeds max size limit |
| 150 | 0x96 | Message rate too high | Publisher rate-limited |
| 151 | 0x97 | Quota exceeded | Account/connection quota reached |
| 152 | 0x98 | Administrative action | Disconnected for administrative reasons |
| 153 | 0x99 | Payload format invalid | Payload Format Indicator says UTF-8 but isn’t |
| 154 | 0x9A | Retain not supported | Retained publish to non-supporting Server |
| 155 | 0x9B | QoS not supported | Requested QoS not available |
| 156 | 0x9C | Use another server | Server redirecting to alternative |
| 157 | 0x9D | Server moved | Permanent Server move |
| 159 | 0x9F | Connection rate exceeded | New connection rate limit hit |
For complete Reason Code coverage with descriptions, see our MQTT 5 Reason Codes Reference.
Where Reason Codes appear
Per the OASIS MQTT v5.0 standard, these control packets carry Reason Codes:
| Packet | Type | Carries Reason Code(s) | Direction |
|---|---|---|---|
| CONNACK | 2 | Single Reason Code | Server → Client |
| PUBACK | 4 | Single Reason Code | Either direction |
| PUBREC | 5 | Single Reason Code | Either direction |
| PUBREL | 6 | Single Reason Code | Either direction |
| PUBCOMP | 7 | Single Reason Code | Either direction |
| SUBACK | 9 | One per topic filter | Server → Client |
| UNSUBACK | 11 | One per topic filter | Server → Client |
| DISCONNECT | 14 | Single Reason Code | Either direction |
| AUTH | 15 | Single Reason Code | Either direction |
Note that PUBLISH, SUBSCRIBE, UNSUBSCRIBE, PINGREQ, PINGRESP, and CONNECT do NOT carry Reason Codes — they are the request packets, not acknowledgement packets. Reason Codes appear only in responses.
Two packets to note specially:
- SUBACK and UNSUBACK carry one Reason Code per topic filter in the original request. If you SUBSCRIBE to 5 topics, the SUBACK contains 5 Reason Codes — telling you the outcome of each subscription individually.
- AUTH uses only three specific Reason Codes (0x00 Success, 0x18 Continue, 0x19 Re-authenticate) — see our Enhanced Authentication in MQTT 5 article.
PUBACK and PUBREC — negative publish acknowledgements
In MQTT 3.1.1, PUBACK was a 2-byte packet: just the Packet Identifier. In MQTT 5, PUBACK is much richer.
PUBACK structure in MQTT 5
Fixed Header:
Byte 1: 01000000 (Type 4, flags 0000)
Remaining length: Variable Byte Integer
Variable Header:
Packet Identifier (2 bytes)
Reason Code (1 byte) ← NEW in MQTT 5
Properties (Variable Byte Integer length + properties) ← NEW in MQTT 5
Payload: None
Per the spec, the Reason Code may be omitted when there are no Properties AND the Reason Code is 0x00 (Success). In that case, PUBACK is the same 2-byte packet as MQTT 3.1.1 — backward compatible on the wire. But when failure occurs or Properties are present, Reason Code is included.
Valid Reason Codes for PUBACK
Per OASIS MQTT v5.0 Section 3.4.2.1, PUBACK can return:
| Code | Hex | Meaning |
|---|---|---|
| 0 | 0x00 | Success |
| 16 | 0x10 | No matching subscribers (informational, not error) |
| 128 | 0x80 | Unspecified error |
| 131 | 0x83 | Implementation specific error |
| 135 | 0x87 | Not authorized |
| 144 | 0x90 | Topic Name invalid |
| 145 | 0x91 | Packet identifier in use |
| 151 | 0x97 | Quota exceeded |
| 153 | 0x99 | Payload format invalid |
PUBREC carries the same Reason Code set
PUBREC (Control Packet 5, the first acknowledgement in QoS 2 flow) accepts the same set of Reason Codes as PUBACK. Failure during the first phase of QoS 2 publish surfaces here.
PUBREL and PUBCOMP have a narrower set
PUBREL (Control Packet 6) and PUBCOMP (Control Packet 7) — the second-phase QoS 2 acknowledgements — accept fewer Reason Codes:
| Code | Hex | Meaning |
|---|---|---|
| 0 | 0x00 | Success |
| 146 | 0x92 | Packet Identifier not found |
The Packet Identifier not found case happens when one side has lost track of the QoS 2 state — usually due to network disruption or session expiry. The receiver gets a PUBREL for a Packet Identifier it doesn’t have a record of.
Practical example — failed PUBLISH
Client → Server: PUBLISH
QoS: 1
Topic: "restricted/secret/data"
Packet Identifier: 123
Payload: "sensitive data"
Server → Client: PUBACK
Packet Identifier: 123
Reason Code: 0x87 (Not authorized)
Properties:
Reason String: "Client is not authorized to publish to topic 'restricted/secret/data'"
The client now knows exactly why the publish failed and can take appropriate action — log the error, alert the operator, retry with different credentials, or stop trying to publish to this topic.
SUBACK and UNSUBACK — subscription results
Subscription operations get per-topic-filter granularity in MQTT 5. If you subscribe to 3 topics, you get 3 Reason Codes back — one for each topic.
SUBACK structure in MQTT 5
Fixed Header:
Byte 1: 10010000 (Type 9, flags 0000)
Remaining length: Variable Byte Integer
Variable Header:
Packet Identifier (2 bytes)
Properties (Variable Byte Integer length + properties)
Payload:
Reason Code 1 (1 byte) ← One per topic filter
Reason Code 2 (1 byte)
...
Reason Code N (1 byte)
Valid SUBACK Reason Codes
Per OASIS MQTT v5.0 Section 3.9.3, each topic filter result can be:
| Code | Hex | Meaning |
|---|---|---|
| 0 | 0x00 | Granted QoS 0 |
| 1 | 0x01 | Granted QoS 1 |
| 2 | 0x02 | Granted QoS 2 |
| 128 | 0x80 | Unspecified error |
| 131 | 0x83 | Implementation specific error |
| 135 | 0x87 | Not authorized |
| 143 | 0x8F | Topic Filter invalid |
| 145 | 0x91 | Packet identifier in use |
| 151 | 0x97 | Quota exceeded |
| 158 | 0x9E | Shared Subscriptions not supported |
| 161 | 0xA1 | Subscription Identifiers not supported |
| 162 | 0xA2 | Wildcard Subscriptions not supported |
Practical example — partial subscription failure
Client → Server: SUBSCRIBE
Packet Identifier: 456
Topic Filters:
"sensors/temperature/+" → Requested QoS 1
"admin/config" → Requested QoS 1
"broken/[#" → Requested QoS 0
Server → Client: SUBACK
Packet Identifier: 456
Reason Codes:
[0]: 0x01 (Granted QoS 1) ← subscribed successfully
[1]: 0x87 (Not authorized) ← admin/config blocked
[2]: 0x8F (Topic Filter invalid) ← bad syntax
The client now knows: temperature subscription works, admin denied (authentication issue), syntax error on third filter. Each one needs different remediation.
UNSUBACK Reason Codes
UNSUBACK has its own narrower set:
| Code | Hex | Meaning |
|---|---|---|
| 0 | 0x00 | Success |
| 17 | 0x11 | No subscription existed |
| 128 | 0x80 | Unspecified error |
| 131 | 0x83 | Implementation specific error |
| 135 | 0x87 | Not authorized |
| 143 | 0x8F | Topic Filter invalid |
| 145 | 0x91 | Packet identifier in use |
The “No subscription existed” code (0x11) is informational — you tried to unsubscribe from something you weren’t subscribed to. Common during error-recovery flows where the client isn’t sure about its current subscription state.
CONNACK — connection rejection reasons
CONNACK is the response to CONNECT. In MQTT 3.1.1 it had a single byte for “Return Code” with five possible values. In MQTT 5 it’s much richer.
CONNACK Reason Codes
Per OASIS MQTT v5.0 Section 3.2.2.2, CONNACK can return:
| Code | Hex | Meaning |
|---|---|---|
| 0 | 0x00 | Success |
| 128 | 0x80 | Unspecified error |
| 129 | 0x81 | Malformed Packet |
| 130 | 0x82 | Protocol Error |
| 131 | 0x83 | Implementation specific error |
| 132 | 0x84 | Unsupported Protocol Version |
| 133 | 0x85 | Client Identifier not valid |
| 134 | 0x86 | Bad User Name or Password |
| 135 | 0x87 | Not authorized |
| 136 | 0x88 | Server unavailable |
| 137 | 0x89 | Server busy |
| 138 | 0x8A | Banned |
| 140 | 0x8C | Bad authentication method |
| 144 | 0x90 | Topic Name invalid |
| 149 | 0x95 | Packet too large |
| 151 | 0x97 | Quota exceeded |
| 153 | 0x99 | Payload format invalid |
| 154 | 0x9A | Retain not supported |
| 155 | 0x9B | QoS not supported |
| 156 | 0x9C | Use another server |
| 157 | 0x9D | Server moved |
| 159 | 0x9F | Connection rate exceeded |
This rich set lets clients respond appropriately:
- 0x84 (Unsupported Protocol Version) → reconnect with older version
- 0x85 (Client Identifier not valid) → regenerate Client ID
- 0x86 (Bad User Name or Password) → re-prompt for credentials
- 0x88 (Server unavailable) → retry with backoff
- 0x9C (Use another server) / 0x9D (Server moved) → redirect to alternate Server
- 0x9F (Connection rate exceeded) → exponential backoff before retry
Server-initiated DISCONNECT — new in MQTT 5
This is one of the biggest practical improvements MQTT 5 brought. In MQTT 3.1.1, only clients sent DISCONNECT packets. When the Server needed to drop a connection, it just closed the TCP socket — leaving the client to guess what happened.
In MQTT 5, the Server can send DISCONNECT with a Reason Code explaining why before closing the connection. This makes Server-side termination diagnosable.
Server DISCONNECT Reason Codes
Per OASIS MQTT v5.0 Section 3.14.2.1, the Server can send DISCONNECT with:
| Code | Hex | Meaning |
|---|---|---|
| 0 | 0x00 | Normal disconnection |
| 4 | 0x04 | Disconnect with Will Message |
| 128 | 0x80 | Unspecified error |
| 129 | 0x81 | Malformed Packet |
| 130 | 0x82 | Protocol Error |
| 131 | 0x83 | Implementation specific error |
| 135 | 0x87 | Not authorized |
| 137 | 0x89 | Server busy |
| 139 | 0x8B | Server shutting down |
| 141 | 0x8D | Keep Alive timeout |
| 142 | 0x8E | Session taken over |
| 143 | 0x8F | Topic Filter invalid |
| 144 | 0x90 | Topic Name invalid |
| 147 | 0x93 | Receive Maximum exceeded |
| 148 | 0x94 | Topic Alias invalid |
| 149 | 0x95 | Packet too large |
| 150 | 0x96 | Message rate too high |
| 151 | 0x97 | Quota exceeded |
| 152 | 0x98 | Administrative action |
| 153 | 0x99 | Payload format invalid |
| 154 | 0x9A | Retain not supported |
| 155 | 0x9B | QoS not supported |
| 156 | 0x9C | Use another server |
| 157 | 0x9D | Server moved |
| 158 | 0x9E | Shared Subscriptions not supported |
| 159 | 0x9F | Connection rate exceeded |
| 160 | 0xA0 | Maximum connect time |
| 161 | 0xA1 | Subscription Identifiers not supported |
| 162 | 0xA2 | Wildcard Subscriptions not supported |
Why this matters operationally
Imagine a client that suddenly loses its MQTT connection in production. In MQTT 3.1.1, the client logs report “connection lost” — and the operator has to dig through Server logs to figure out why. In MQTT 5, the client received DISCONNECT with Reason Code 0x97 (Quota exceeded) — the client logs explicitly say what happened.
This single change reduces troubleshooting time dramatically. Common Server-initiated DISCONNECT scenarios:
- 0x8D (Keep Alive timeout): client didn’t send PINGREQ in time
- 0x8E (Session taken over): another client connected with the same Client Identifier
- 0x97 (Quota exceeded): account hit messaging quota
- 0x96 (Message rate too high): client publishing too fast
- 0x9F (Connection rate exceeded): too many connections too quickly
- 0xA0 (Maximum connect time): Server enforces a max connection duration
The “No matching subscribers” special case (0x10)
This is an interesting edge case. Reason Code 0x10 (No matching subscribers) is in the success range (≤0x7F) — but it’s informational, not a success per se.
The scenario: a client publishes with QoS 1 or QoS 2 to a topic that has no subscribers. The publish technically “succeeded” — the Server received and processed it correctly — but no message was actually delivered to anyone because nobody was listening.
The Server returns PUBACK or PUBREC with Reason Code 0x10 to tell the publisher “your message was received but nobody got it.” This is valuable diagnostic information for:
- Detecting when no consumers exist for a published topic (configuration error?)
- Monitoring system health (subscriptions broken?)
- Optimizing publish flows (no need to publish if nobody’s listening)
- Tracking down “messages going nowhere” issues during development
Per the spec, this is optional behavior — Servers may choose to return 0x00 (Success) even when no subscribers exist. Server-specific configuration typically controls this.
Reason String and User Property additions
Beyond the Reason Code itself, MQTT 5 acknowledgement packets can include optional properties for richer diagnostic information:
Reason String (Property 0x1F)
A UTF-8 string providing human-readable context for the Reason Code. Examples:
PUBACK with Reason Code 0x87:
Reason String: "Client 'sensor-042' is not authorized to publish to topic 'admin/config'"
SUBACK with Reason Code 0x9E:
Reason String: "Shared subscriptions are disabled on this server"
DISCONNECT with Reason Code 0x97:
Reason String: "Account 'tenant-foo' has exceeded its message quota for the current billing period"
Reason String is optional — Servers may include it or not. When included, it’s useful for logging and user-facing error messages. It is not intended for programmatic interpretation — the Reason Code itself drives programmatic decisions.
User Property (Property 0x26)
A UTF-8 name-value pair providing structured additional context. Multiple User Properties can appear in one acknowledgement.
Examples:
PUBACK with Reason Code 0x97 (Quota exceeded):
User Property: ("quota-type", "messages-per-hour")
User Property: ("quota-limit", "10000")
User Property: ("quota-reset-at", "2025-01-15T00:00:00Z")
User Properties are vendor-specific — they’re not part of the MQTT spec’s core but allow extension. Server implementations use them to communicate custom diagnostic information, billing details, rate limit data, etc.
Failure handling patterns
How should client applications use negative acknowledgements? Some practical patterns:
Pattern 1: Categorize by Reason Code class
def handle_puback(packet):
rc = packet.reason_code
if rc < 0x80:
# Success or informational (0x00, 0x10)
log_success(packet)
return
if rc in [0x87]: # Not authorized
# Permanent failure — don't retry, alert operator
alert_authorization_failure(packet)
return
if rc in [0x88, 0x89, 0x97]: # Transient failures
# Retry with exponential backoff
schedule_retry(packet, exponential_backoff())
return
if rc in [0x90, 0x99]: # Client-side errors
# Don't retry, fix the publish
log_client_error(packet)
return
# Unknown failure — log and investigate
log_unknown_failure(packet)
Pattern 2: Use Reason String for logging
Always log the Reason String when present — it gives immediate context that the Reason Code alone doesn’t:
log.error(f"PUBLISH failed: reason_code={rc}, reason_string={packet.reason_string}")
This makes logs immediately useful for troubleshooting without requiring engineers to look up Reason Code meanings.
Pattern 3: Handle Server-initiated DISCONNECT gracefully
def on_server_disconnect(packet):
rc = packet.reason_code
if rc == 0x9C: # Use another server
server_ref = packet.properties.get('server-reference')
reconnect_to(server_ref)
elif rc == 0x8D: # Keep Alive timeout
# Was our network connection slow? Reconnect.
reconnect_with_lower_keepalive()
elif rc == 0x8E: # Session taken over
# Someone else stole our Client ID
regenerate_client_id_and_reconnect()
elif rc == 0x97: # Quota exceeded
# Don't reconnect immediately - quota needs to reset
wait_for_quota_reset_then_reconnect()
# ... other cases ...
This produces much smarter automatic recovery than MQTT 3.1.1’s “TCP closed, just reconnect” pattern.
Backward compatibility considerations
When deploying MQTT 5 alongside MQTT 3.1.1 systems:
MQTT 3.1.1 client connecting to MQTT 5 Server
The Server must downgrade its responses to MQTT 3.1.1 semantics. PUBACK reverts to 2-byte form (no Reason Code). SUBACK uses MQTT 3.1.1’s return code byte values. The Server can’t communicate the rich Reason Code context to a v3.1.1 client.
MQTT 5 client connecting to MQTT 3.1.1 Server
The Server doesn’t support MQTT 5 features. The client’s CONNECT will return CONNACK with Reason Code 0x84 (Unsupported Protocol Version) — and the client should reconnect with MQTT 3.1.1 if it wants to talk to that Server.
Mixed-version Server clusters
In a cluster of Servers where some support MQTT 5 and some don’t, clients can be redirected via DISCONNECT Reason Code 0x9C (Use another server) with the new Server reference in the Server Reference property. The client reconnects to the indicated Server, which supports its preferred version.
For broader version compatibility discussion, see our MQTT 5 vs MQTT 3.1.1 Comparison article.
Common errors and troubleshooting
| Symptom | Likely Reason Code | What to do |
|---|---|---|
| Client repeatedly disconnects | 0x8D Keep Alive timeout | Decrease Keep Alive interval; check network |
| Publish always fails to one topic | 0x87 Not authorized | Verify topic-level authorization rules |
| Subscribe to wildcard fails | 0xA2 Wildcard not supported | Server may have wildcards disabled — check policy |
| Random publishes return failure | 0x96 Message rate too high | Implement client-side rate limiting |
| Connection lost without reconnect possible | 0x8A Banned | Client banned by admin — contact operator |
| Server randomly disconnects clients | 0x97 Quota exceeded | Account hit messaging quota |
| Some subscriptions succeed, some don’t | 0x87 + others | Review per-topic auth in SUBACK |
| Reconnect immediately fails | 0x9F Connection rate exceeded | Implement exponential backoff |
| Publish OK but no delivery | 0x10 No matching subscribers | No consumers — check subscription setup |
| All operations failing on reconnect | 0x8E Session taken over | Another client took your Client ID |
Frequently asked questions
What are MQTT 5 negative acknowledgements?
MQTT 5 negative acknowledgements are the mechanism by which Servers signal failure conditions through Reason Codes carried in acknowledgement packets. Per the OASIS MQTT v5.0 standard, Reason Codes are single-byte values where 0x00-0x7F indicate success and 0x80-0xFF indicate failure. Every MQTT 5 acknowledgement packet (CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, AUTH, DISCONNECT) carries a Reason Code. This replaces MQTT 3.1.1’s silent failure model with explicit, programmatic error signaling.
What is the difference between MQTT 3.1.1 and MQTT 5 acknowledgements?
MQTT 3.1.1 acknowledgements primarily confirmed receipt without communicating failure reasons. PUBACK was just a packet identifier; SUBACK returned 0x80 for any failure; CONNACK had only five possible return codes. MQTT 5 added a Reason Code byte to every acknowledgement, expanded the failure codes to over 30 specific reasons, introduced per-topic-filter results in SUBACK, added Server-initiated DISCONNECT with reasons, and added optional Reason String and User Property properties for additional context.
What Reason Codes can a PUBACK return in MQTT 5?
Per OASIS MQTT v5.0 Section 3.4.2.1, PUBACK can return: 0x00 (Success), 0x10 (No matching subscribers, informational), 0x80 (Unspecified error), 0x83 (Implementation specific error), 0x87 (Not authorized), 0x90 (Topic Name invalid), 0x91 (Packet identifier in use), 0x97 (Quota exceeded), or 0x99 (Payload format invalid). The same set applies to PUBREC.
What does Reason Code 0x10 (No matching subscribers) mean?
Reason Code 0x10 is informational — it’s returned in PUBACK or PUBREC when a QoS 1 or QoS 2 publish was received and processed by the Server, but no subscribers existed for the topic. The publish technically succeeded, but no message was delivered. This is valuable diagnostic information for detecting broken subscription setups or unused topics. Note that 0x10 is in the success range (≤0x7F), not failure.
Can the Server send a DISCONNECT in MQTT 5?
Yes. This is one of the most operationally significant additions in MQTT 5. In MQTT 3.1.1, only clients could send DISCONNECT packets; Servers terminating a connection just closed the TCP socket without explanation. In MQTT 5, the Server can send DISCONNECT with a Reason Code explaining why (Keep Alive timeout, Quota exceeded, Banned, Session taken over, etc.) before closing the connection. This makes Server-side termination diagnosable in client logs.
What does Reason Code 0x87 (Not authorized) mean?
Reason Code 0x87 indicates authentication succeeded but the operation is not authorized for this client. The Server recognized the client identity (the credentials are valid) but the client lacks permission for the specific action — typically publishing or subscribing to a topic they’re not allowed to access. This is distinct from 0x86 (Bad User Name or Password), which indicates authentication itself failed.
How does SUBACK handle multiple topic filters in MQTT 5?
SUBACK contains one Reason Code per topic filter in the original SUBSCRIBE request. If a client subscribes to 3 topics, SUBACK returns 3 Reason Codes — one per topic, in the same order. Each can be 0x00/0x01/0x02 (Granted QoS 0/1/2) or a specific failure code (0x87 Not authorized, 0x8F Topic Filter invalid, etc.). This per-filter granularity lets clients know exactly which subscriptions succeeded and which failed.
What is the Reason String property in MQTT 5?
Reason String (Property 0x1F) is an optional UTF-8 string accompanying a Reason Code in acknowledgement packets. It provides human-readable context for the Reason Code — example: a 0x87 (Not authorized) PUBACK might include a Reason String like “Client ‘sensor-042’ is not authorized to publish to topic ‘admin/config’.” Reason String is intended for logging and human display, not programmatic interpretation. The Reason Code itself drives programmatic decisions.
How should client code handle MQTT 5 Reason Codes?
Programmatic handling should categorize by Reason Code class: success codes (≤0x7F) indicate success; failure codes (≥0x80) indicate failure. Within failures, handle permanent failures (0x87 Not authorized) differently from transient failures (0x88 Server unavailable, 0x97 Quota exceeded). Always log the Reason Code value and Reason String for troubleshooting. Server-initiated DISCONNECT codes should drive intelligent reconnection logic — exponential backoff for rate limits, immediate redirect for “Use another server” (0x9C).
What is Reason Code 0x8E (Session taken over)?
Reason Code 0x8E indicates that another client connected to the same Server using the same Client Identifier. Per the spec, the Server must close the older connection when a new connection arrives with a duplicate Client Identifier (after Clean Start = false handling). The older connection receives DISCONNECT with 0x8E. Clients receiving this code should investigate why their Client Identifier was reused — typically a configuration error or a misbehaving client.
Can MQTT 3.1.1 clients understand MQTT 5 Reason Codes?
No. MQTT 3.1.1 clients don’t understand MQTT 5’s Reason Code structure beyond the limited set in CONNACK (5 return codes). When an MQTT 3.1.1 client connects to an MQTT 5 Server, the Server must downgrade its responses to MQTT 3.1.1 semantics — PUBACK becomes the 2-byte form, SUBACK uses 3.1.1’s return code values. The MQTT 5 Reason Code richness only works between MQTT 5 clients and MQTT 5 Servers.
