Skip to main content Skip to sidebar

MQTT v3.1.1 vs v5

MQTT is the de-facto messaging protocol for IoT, connecting everything from battery-powered sensors to industrial gateways. For over a decade, MQTT v3.1.1 has been the workhorse: simple, lightweight, and well supported. MQTT v5, published in 2019, is the first major revision and brings features that address real operational pain points around observability, flow control, and error reporting.

This post compares the two versions, explains what v5 actually adds, and helps you decide whether a migration is worth the effort.

What is MQTT

MQTT (Message Queuing Telemetry Transport) is a publish/subscribe protocol that runs over TCP (or WebSockets). Clients connect to a broker, publish messages to topics, and subscribe to topic filters. The broker handles routing and delivery guarantees.

flowchart LR
    P1["Publisher 1<br/>sensor/temp"] -->|publish| Broker
    P2["Publisher 2<br/>sensor/humidity"] -->|publish| Broker
    Broker -->|deliver| S1["Subscriber A<br/>sensor/#"]
    Broker -->|deliver| S2["Subscriber B<br/>sensor/temp"]

    style Broker fill:#d4edda,stroke:#28a745,stroke-width:1px
    style P1 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style P2 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style S1 fill:#fff3cd,stroke:#ffc107,stroke-width:1px
    style S2 fill:#fff3cd,stroke:#ffc107,stroke-width:1px

MQTT defines three Quality of Service (QoS) levels:

  • QoS 0 - at most once, fire and forget
  • QoS 1 - at least once, acknowledged delivery with possible duplicates
  • QoS 2 - exactly once, four-step handshake

These levels apply independently to publish and subscribe operations.

MQTT v3.1.1

MQTT v3.1.1 was standardized as OASIS Standard in 2014 and later published as ISO/IEC 20922:2016. It is the version most brokers, client libraries, and embedded stacks still default to.

The protocol fits into 14 control packet types and a fixed two-byte header. A CONNECT packet carries client ID, credentials, keep-alive, and an optional Last Will and Testament (LWT) message. Everything else is minimal: PUBLISH, SUBSCRIBE, PINGREQ, DISCONNECT, and a handful of acknowledgements.

Strengths: small footprint, trivially implementable on microcontrollers, battle-tested in production for over a decade, broad ecosystem support.

Weaknesses: limited error reporting (a failed CONNECT returns a single byte with five possible values), no server-to-client flow control, no way to attach metadata to messages, no response topic for request/reply patterns, and no standard way to expire messages.

MQTT v5

MQTT v5 was published as an OASIS Standard in 2019. It keeps the same publish/subscribe model and QoS levels but adds a properties system to every packet, enabling features that previously required out-of-band conventions.

The wire format stays close to v3.1.1. Existing v3.1.1 packets remain recognizable; v5 adds a length-prefixed properties section after the variable header and before the payload.

block-beta
    columns 4

    fixed["Fixed Header<br/>(1-5 bytes)"]
    variable["Variable Header"]
    properties["Properties<br/>(v5 only)"]
    payload["Payload"]

    style fixed fill:#d4edda,stroke:#28a745,stroke-width:1px
    style variable fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style properties fill:#fff3cd,stroke:#ffc107,stroke-width:1px
    style payload fill:#f8d7da,stroke:#dc3545,stroke-width:1px

Key additions include reason codes on every acknowledgement, user properties (arbitrary key-value metadata), shared subscriptions, topic aliases, session expiry, message expiry, request/response semantics, and flow control via receive maximum.

Feature Comparison

Featurev3.1.1v5
Control packet types1415 (adds AUTH)
Reason codesLimited (CONNACK only, 5 values)All acks, 40+ codes
User propertiesNoYes, arbitrary key-value pairs
Session expiryClean session flag onlyConfigurable interval
Message expiryNoPer-message interval
Topic aliasesNoInteger alias replaces topic string
Shared subscriptionsNo (broker-specific extensions)Standard $share/{group}/topic
Request/responseConvention onlyResponse Topic + Correlation Data
Flow controlNoReceive Maximum
Maximum packet sizeImplicit broker limitNegotiated
Server redirectionNoServer Reference in DISCONNECT
Enhanced authenticationNoAUTH packet, SASL-style exchange
Will delaySent immediately on disconnectDelay interval before sending
Payload format indicatorNoUTF-8 or binary hint
Content typeNoMIME-like string

Reason Codes

In v3.1.1, if a CONNECT fails the client receives a CONNACK with one of five return codes (unacceptable protocol version, identifier rejected, server unavailable, bad credentials, not authorized). A failed PUBLISH at QoS 1 or 2 gives no reason at all, only an acknowledgement or a timeout.

In v5, every acknowledgement packet (CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH) carries a one-byte reason code. The specification defines more than forty codes covering authentication failures, quota exceeded, payload format invalid, topic name invalid, packet too large, and many more. Each reason code can be accompanied by a human-readable Reason String and arbitrary User Properties.

For operators, this turns silent failures into actionable telemetry. A client no longer has to guess why the broker closed the connection.

User Properties

User Properties are UTF-8 key-value pairs attached to any packet type. They carry application-level metadata without encoding it inside the payload.

Typical uses:

  • Tracing and correlation IDs propagated alongside a message
  • Tenant identifiers in multi-tenant brokers
  • Schema version hints for the payload
  • Routing hints consumed by bridges or gateways

Because properties live in the header, subscribers can inspect them without parsing the payload, and brokers can route or filter on them if they choose.

Shared Subscriptions

MQTT v3.1.1 delivers every matching message to every subscriber. To scale a consumer horizontally you had to rely on broker-specific extensions (EMQX, HiveMQ, Mosquitto all implemented variations).

MQTT v5 standardizes the syntax $share/{group}/{topic-filter}. Subscribers that join the same group receive messages in a round-robin or broker-chosen distribution, while subscribers outside the group still receive every message.

flowchart LR
    Broker -->|msg 1| C1["Consumer 1<br/>$share/workers/jobs"]
    Broker -->|msg 2| C2["Consumer 2<br/>$share/workers/jobs"]
    Broker -->|msg 3| C3["Consumer 3<br/>$share/workers/jobs"]
    Broker -->|all msgs| M["Monitor<br/>jobs"]

    style Broker fill:#d4edda,stroke:#28a745,stroke-width:1px
    style C1 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style C2 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style C3 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style M fill:#fff3cd,stroke:#ffc107,stroke-width:1px

This makes MQTT a viable work-queue transport without bolting on a second message broker.

Topic Aliases

Topic strings in v3.1.1 repeat on every PUBLISH. A device reporting to factory/line-3/station-7/sensor/temperature pays that 45-byte cost on every message.

In v5, a publisher can assign an integer alias the first time it publishes to a topic and then use the alias on subsequent messages. The broker maintains the mapping per connection. For chatty devices on narrowband links, this measurably reduces bandwidth.

The maximum alias value is negotiated in CONNECT and CONNACK via the Topic Alias Maximum property.

Session and Message Expiry

v3.1.1 offers only a Clean Session flag: either the broker keeps subscription state and queued messages forever, or it discards everything at disconnect. There is no middle ground.

v5 replaces this with a Session Expiry Interval (seconds) and a Message Expiry Interval (per message). Sessions can persist for a day but not forever. Messages can be marked stale after five minutes so sleeping subscribers do not wake to a flood of obsolete data.

This single change removes a class of operational issues around abandoned sessions consuming broker memory.

Request/Response

MQTT is pub/sub, but many applications need request/response patterns: a command to a device that expects an acknowledgement, a query to a service that returns a value.

In v3.1.1, teams built this by convention. The requester would include a response topic in the payload, subscribe to it, and the responder would parse the payload to learn where to reply.

v5 promotes this to protocol level with two properties on PUBLISH:

  • Response Topic - topic string the responder should publish its reply to
  • Correlation Data - opaque bytes the requester uses to match responses

Both are in the header, so responders do not need to understand the payload format.

Flow Control

v3.1.1 has no mechanism for a subscriber to tell a broker “slow down.” If the broker sends faster than the client can process, TCP backpressure eventually kicks in, but by then large queues have built up.

v5 adds the Receive Maximum property: the maximum number of QoS 1 and QoS 2 PUBLISH packets the client is willing to have in flight without acknowledgement. The broker must respect it. This gives embedded clients with tiny buffers a standardized way to avoid being overwhelmed.

Migration Considerations

Brokers and libraries that support v5 typically continue to accept v3.1.1 connections on the same port. The CONNECT packet carries a protocol level byte (4 for v3.1.1, 5 for v5), so a single broker can serve both versions simultaneously.

A gradual migration usually looks like:

  • Upgrade the broker to a version that supports MQTT v5 while keeping v3.1.1 compatibility
  • Upgrade client libraries on the server and gateway side first
  • Start using v5 features (reason codes, user properties) in new deployments
  • Migrate field devices opportunistically, during scheduled firmware updates

Gotchas to watch for:

  • Some brokers have subtle differences in how they handle session expiry across reconnects
  • Client libraries vary in how they surface reason codes to application code
  • Topic alias state is per connection; a reconnect resets all aliases
  • Shared subscriptions change delivery semantics - make sure downstream logic tolerates out-of-order messages across workers

Go Implementation: mqttv5

For Go projects that need a v5-native client or broker, I built github.com/vitalvas/mqttv5 from scratch, an MQTT v5.0 SDK that implements the full OASIS standard. It covers all 15 control packet types, the complete properties system (42 property identifiers), QoS 0/1/2 state machines, topic wildcards, shared subscriptions, retained and will messages, flow control, and keep-alive management.

The library exposes both client and server APIs from the same package, supports TCP, TLS, WebSocket, WSS, Unix socket, and QUIC transports, and includes pluggable authentication, multi-tenancy with namespace isolation, message interceptors, broker bridging, and rate limiting.

Note: MQTT v3.1.1 support will be added soon, so the same library will work for mixed-version deployments.

A minimal client looks like this:

package main

import (
    "fmt"
    "github.com/vitalvas/mqttv5"
)

func main() {
    client, err := mqttv5.Dial(
        mqttv5.WithServers("tcp://localhost:1883"),
        mqttv5.WithClientID("my-client"),
        mqttv5.WithKeepAlive(60),
    )
    if err != nil {
        panic(err)
    }
    defer client.Close()

    client.Subscribe("sensors/#", 1, func(msg *mqttv5.Message) {
        fmt.Printf("Received: %s\n", msg.Payload)
    })

    client.Publish(&mqttv5.Message{
        Topic:   "sensors/temperature",
        Payload: []byte("23.5"),
        QoS:     mqttv5.QoS1,
    })
}

And a broker built from the same package:

package main

import (
    "log"
    "net"
    "github.com/vitalvas/mqttv5"
)

func main() {
    listener, _ := net.Listen("tcp", ":1883")

    srv := mqttv5.NewServer(
        mqttv5.WithListener(listener),
        mqttv5.OnConnect(func(c *mqttv5.ServerClient) {
            log.Printf("Client connected: %s", c.ClientID())
        }),
        mqttv5.OnMessage(func(c *mqttv5.ServerClient, m *mqttv5.Message) {
            log.Printf("Message: %s -> %s", m.Topic, m.Payload)
        }),
    )

    srv.ListenAndServe()
}

References