Server

MQTT QoS Levels Explained

By Prabath Thalangama· October 24, 2026· 4 min read
#mqtt#iot#messaging

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-id changed 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.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.