MQTT 5 Negative Acknowledgements: Reason Codes Explained

By | June 29, 2026

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.

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:

RangeMeaning
0x00 – 0x7FSuccess or informational (positive acknowledgement)
0x80 – 0xFFFailure 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:

CodeHexNameCommon Cause
1280x80Unspecified errorServer can’t determine specific reason
1290x81Malformed PacketPacket doesn’t conform to spec
1300x82Protocol ErrorLogical protocol violation
1310x83Implementation specific errorServer-side issue not in standard codes
1340x86Bad User Name or PasswordAuthentication failed
1350x87Not authorizedAuthentication OK but not authorized
1360x88Server unavailableServer transient unavailability
1370x89Server busyServer overloaded
1380x8ABannedClient banned by administrative action
1390x8BServer shutting downServer graceful shutdown
1410x8DKeep Alive timeoutClient missed Keep Alive
1420x8ESession taken overSame Client Identifier connected elsewhere
1430x8FTopic Filter invalidSubscription topic format wrong
1440x90Topic Name invalidPublish topic format wrong
1450x91Packet identifier in useReuse of active Packet Identifier
1460x92Packet Identifier not foundReference to non-existent Packet ID
1470x93Receive Maximum exceededToo many in-flight QoS messages
1480x94Topic Alias invalidTopic Alias number out of range or unmapped
1490x95Packet too largePacket exceeds max size limit
1500x96Message rate too highPublisher rate-limited
1510x97Quota exceededAccount/connection quota reached
1520x98Administrative actionDisconnected for administrative reasons
1530x99Payload format invalidPayload Format Indicator says UTF-8 but isn’t
1540x9ARetain not supportedRetained publish to non-supporting Server
1550x9BQoS not supportedRequested QoS not available
1560x9CUse another serverServer redirecting to alternative
1570x9DServer movedPermanent Server move
1590x9FConnection rate exceededNew 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:

PacketTypeCarries Reason Code(s)Direction
CONNACK2Single Reason CodeServer → Client
PUBACK4Single Reason CodeEither direction
PUBREC5Single Reason CodeEither direction
PUBREL6Single Reason CodeEither direction
PUBCOMP7Single Reason CodeEither direction
SUBACK9One per topic filterServer → Client
UNSUBACK11One per topic filterServer → Client
DISCONNECT14Single Reason CodeEither direction
AUTH15Single Reason CodeEither 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:

CodeHexMeaning
00x00Success
160x10No matching subscribers (informational, not error)
1280x80Unspecified error
1310x83Implementation specific error
1350x87Not authorized
1440x90Topic Name invalid
1450x91Packet identifier in use
1510x97Quota exceeded
1530x99Payload 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:

CodeHexMeaning
00x00Success
1460x92Packet 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:

CodeHexMeaning
00x00Granted QoS 0
10x01Granted QoS 1
20x02Granted QoS 2
1280x80Unspecified error
1310x83Implementation specific error
1350x87Not authorized
1430x8FTopic Filter invalid
1450x91Packet identifier in use
1510x97Quota exceeded
1580x9EShared Subscriptions not supported
1610xA1Subscription Identifiers not supported
1620xA2Wildcard 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:

CodeHexMeaning
00x00Success
170x11No subscription existed
1280x80Unspecified error
1310x83Implementation specific error
1350x87Not authorized
1430x8FTopic Filter invalid
1450x91Packet 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:

CodeHexMeaning
00x00Success
1280x80Unspecified error
1290x81Malformed Packet
1300x82Protocol Error
1310x83Implementation specific error
1320x84Unsupported Protocol Version
1330x85Client Identifier not valid
1340x86Bad User Name or Password
1350x87Not authorized
1360x88Server unavailable
1370x89Server busy
1380x8ABanned
1400x8CBad authentication method
1440x90Topic Name invalid
1490x95Packet too large
1510x97Quota exceeded
1530x99Payload format invalid
1540x9ARetain not supported
1550x9BQoS not supported
1560x9CUse another server
1570x9DServer moved
1590x9FConnection 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:

CodeHexMeaning
00x00Normal disconnection
40x04Disconnect with Will Message
1280x80Unspecified error
1290x81Malformed Packet
1300x82Protocol Error
1310x83Implementation specific error
1350x87Not authorized
1370x89Server busy
1390x8BServer shutting down
1410x8DKeep Alive timeout
1420x8ESession taken over
1430x8FTopic Filter invalid
1440x90Topic Name invalid
1470x93Receive Maximum exceeded
1480x94Topic Alias invalid
1490x95Packet too large
1500x96Message rate too high
1510x97Quota exceeded
1520x98Administrative action
1530x99Payload format invalid
1540x9ARetain not supported
1550x9BQoS not supported
1560x9CUse another server
1570x9DServer moved
1580x9EShared Subscriptions not supported
1590x9FConnection rate exceeded
1600xA0Maximum connect time
1610xA1Subscription Identifiers not supported
1620xA2Wildcard 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

SymptomLikely Reason CodeWhat to do
Client repeatedly disconnects0x8D Keep Alive timeoutDecrease Keep Alive interval; check network
Publish always fails to one topic0x87 Not authorizedVerify topic-level authorization rules
Subscribe to wildcard fails0xA2 Wildcard not supportedServer may have wildcards disabled — check policy
Random publishes return failure0x96 Message rate too highImplement client-side rate limiting
Connection lost without reconnect possible0x8A BannedClient banned by admin — contact operator
Server randomly disconnects clients0x97 Quota exceededAccount hit messaging quota
Some subscriptions succeed, some don’t0x87 + othersReview per-topic auth in SUBACK
Reconnect immediately fails0x9F Connection rate exceededImplement exponential backoff
Publish OK but no delivery0x10 No matching subscribersNo consumers — check subscription setup
All operations failing on reconnect0x8E Session taken overAnother 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.

Author: Zakaria El Intissar

I've spent 13 years in power system automation, electrical protection, and SCADA communication, as an automation and industrial computing engineer. ScadaProtocols.com is where I turn what I've learned on site into plain guides and working tools — so other engineers can decode, analyze, and troubleshoot industrial communication protocols without the guesswork.