MQTT QoS Levels Explained
Introduction¶
MQTT is a lightweight publish/subscribe protocol over TCP, ubiquitous in IoT. Its three Quality of Service levels control the delivery guarantee between a client and the broker — not end to end from publisher to subscriber. Understanding that distinction, and the packet flow behind each level, prevents a lot of "why did I get this message twice?" confusion.
The three levels¶
QoS 0 — at most once ("fire and forget")¶
PUBLISH ---->
One packet, no acknowledgement, no retry. If the TCP connection drops mid-send, the message is gone. Lowest overhead. Use for high-frequency telemetry where the next reading supersedes a lost one (a temperature sensor publishing every second).
QoS 1 — at least once¶
PUBLISH (id=42) ---->
<---- PUBACK (id=42)
The sender stores the message, sends it with a packet ID, and keeps retransmitting (with the DUP flag set) until it gets a PUBACK. If the PUBACK is lost, the sender retransmits and the receiver sees the message twice. Guaranteed delivery, possible duplicates. The subscriber must be idempotent.
QoS 2 — exactly once¶
PUBLISH (id=42) ---->
<---- PUBREC (id=42)
PUBREL (id=42) ---->
<---- PUBCOMP (id=42)
A four-packet handshake. The receiver records the packet ID on PUBREC and won't process a second PUBLISH with the same ID; PUBREL/PUBCOMP complete the transaction and release the ID. No loss, no duplicates, highest latency and state. Use for commands that must not be repeated (billing events, "unlock the door").
The subtlety: QoS is per-hop¶
A publisher sends at QoS 2. A subscriber subscribed at QoS 0. What does the
subscriber get? QoS 0 — the broker delivers at min(publish QoS, subscription
QoS). The exactly-once guarantee applied only between the publisher and the
broker; from broker to that subscriber it's fire-and-forget.
So "exactly once, end to end" requires QoS 2 on both the publish and every subscription, plus a broker that persists state. In practice most systems use QoS 1 everywhere and dedupe in the application.
Retained messages and QoS¶
A message published with the retain flag is stored by the broker (one per topic) and delivered immediately to any future subscriber. QoS still applies to that delivery. Retained is how a new subscriber learns "the last known state" of a topic without waiting for the next publish. Publish an empty payload with retain to clear it.
Clean session / persistent session¶
- Clean session = true (MQTT 3.1.1) / Clean Start = true (MQTT 5): the broker discards all subscription and queued-message state on disconnect. A reconnecting client starts fresh.
- Clean session = false: the broker keeps the client's subscriptions and
queues QoS ½ messages published while it was offline, delivering them on
reconnect. Requires a stable
client-id. This is how you get "offline buffering" — but only for QoS ≥ 1.
MQTT 5 replaces the binary flag with Session Expiry Interval (seconds), which is more flexible.
Hands-on¶
# Subscribe at QoS 2, verbose (mosquitto clients)
mosquitto_sub -h broker.example.com -t "sensors/#" -q 2 -v -d
# Publish retained, QoS 1
mosquitto_pub -h broker.example.com -t "sensors/room1/temp" -m "21.4" -q 1 -r
# Persistent session: fixed id, clean-session off, then kill and reconnect
mosquitto_sub -h broker -i my-device-01 -c -t "cmd/my-device-01/#" -q 1
# Watch the broker's view (mosquitto)
mosquitto_sub -h broker -t '$SYS/broker/messages/#' -v
mosquitto_sub -h broker -t '$SYS/broker/clients/connected' -v
Verification and troubleshooting¶
- Subscriber receives every message twice — QoS 1 with PUBACKs being lost
(flaky link), or two subscribers sharing a
client-id(they keep kicking each other off and redelivering). Give each client a unique id; dedupe on an application message id. - Messages published while a device was offline never arrive — the device
used clean-session/clean-start = true, or QoS 0 (never queued), or the session
expired, or the
client-idchanged on reconnect. - New subscriber gets nothing until the next publish — no retained message
on that topic. Publish state with
-r. - QoS 2 command executed twice — a subscriber subscribed at QoS 0 or 1, or the application re-processes on reconnect. Verify the SUBACK granted QoS 2 and make the handler idempotent anyway.
- Broker memory climbing — persistent sessions for devices that never come back, queuing QoS ½ messages forever. Set a session expiry / max queued messages, and use clean sessions for transient clients.
- High latency on a chatty topic — QoS 2's four-packet handshake per message. Drop to QoS 1 or 0 if the guarantee isn't needed.
Related tools and reading¶
- On-site: Port Checker (1883 / 8883), SSL Certificate Checker (for TLS on 8883).
- Related posts: WebSocket vs long polling, The TCP three-way handshake explained.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Network Protocols
DHCP DORA: How a Host Gets Its Address
The four DHCP packets, the renewal timers, why relays exist, and the tcpdump filter to watch it happen.
October 24, 2026SMB Versions and Signing, Demystified
SMB1 is a security liability, SMB3 encrypts and signs. Here's how dialect negotiation works and how to tell which one a connection actually used.
October 24, 2026DNS over TLS and DNS over HTTPS
Plain DNS is unencrypted and unauthenticated. DoT and DoH fix the transport — here's how each works, and the trade-off between privacy and network visibility.
October 24, 2026