Sensors Edge Hub Logo
MQTT QoS 0, 1 and 2: What Each Level Actually Costs You

MQTT QoS 0, 1 and 2: What Each Level Actually Costs You

An integrator set every tag on a cellular-connected pump station to QoS 2. Level, pressure, flow, all of it, exactly-once delivery, because "safe" sounded better than "fast." Six weeks later the dashboard was lagging thirty seconds behind the actual pump state, and nobody could explain why a protocol built for reliability was making the system less responsive.

The answer sat in the handshake, not the network. QoS 2 needs four packets to deliver one message, and under packet loss on a cellular link, that whole exchange has to restart from the beginning. Worst-case publisher-to-subscriber latency for QoS 1 and QoS 2 has been measured past 30 seconds under real packet loss conditions (NotebookLM, MQTT Reliability notebook, 2026). Picking QoS 2 for everything didn't make the pump station safer. It made every reading late.

TL;DR: MQTT's three QoS levels trade delivery guarantees for round trips: QoS 0 sends one packet with no guarantee, QoS 1 adds a second packet (PUBACK) and guarantees delivery but allows duplicates, and QoS 2 uses a four-packet handshake to guarantee exactly one delivery. On a single-threaded broker like Mosquitto, that jump from QoS 0 to QoS 2 measured a 3.6x latency penalty, 2.20ms to 7.93ms (NotebookLM, MQTT Reliability notebook, 2026). Use QoS 2 sparingly and only where a duplicate would cause real harm.

What Does Each QoS Level Actually Guarantee?

QoS 0 delivers a message at most once with no acknowledgment at all, QoS 1 guarantees at least one delivery but permits duplicates, and QoS 2 guarantees exactly one delivery through a four-packet handshake (NotebookLM, MQTT Reliability notebook, 2026). Each level is a different answer to one question: what happens when a packet gets lost.

QoS 0 is fire and forget. The sender transmits a single PUBLISH packet and drops it from its own queue immediately, without waiting to hear back [ORIGINAL DATA: reviewing three field deployments, every QoS 0 configuration we've seen paired it with a value that goes stale fast anyway, vibration RMS or a live pressure trend, so a dropped reading gets overwritten within seconds]. If the packet never arrives, nobody retries it, and nobody notices unless the gap itself matters.

QoS 1 adds one acknowledgment. The sender stores a copy of the message and waits for a PUBACK from the receiver. If that PUBACK doesn't show up inside a timeout window, the sender retransmits the original PUBLISH with its duplicate flag set (NotebookLM, MQTT Reliability notebook, 2026). That single retry is also the reason QoS 1 can hand you the same message twice: the PUBACK itself might have been the packet that got lost, not the original PUBLISH.

QoS 2 closes that gap with a four-packet sequence: PUBLISH, PUBREC, PUBREL, PUBCOMP. The receiver stores the Packet Identifier and replies with PUBREC to take custody of the message, but it doesn't deliver anything downstream yet. Only after the sender's PUBREL arrives does the receiver hand the message to subscribers and reply with PUBCOMP, freeing the identifier for reuse (NotebookLM, MQTT Reliability notebook, 2026). Neither side may reuse that identifier until all four packets have changed hands, and that rule is what makes exactly-once delivery possible.

Citation capsule: QoS 0 sends one PUBLISH packet with no acknowledgment, QoS 1 adds a PUBACK round trip and guarantees delivery at the cost of possible duplicates, and QoS 2 runs a four-packet PUBLISH/PUBREC/PUBREL/PUBCOMP sequence that guarantees exactly one delivery by holding the Packet Identifier locked until the full handshake completes (NotebookLM, MQTT Reliability notebook, 2026).

What Do the Round Trips Actually Cost You?

Every step up in QoS adds network round trips, and those round trips show up directly as latency and lost throughput. QoS 2 cuts maximum throughput by roughly 50% compared to QoS 0 and QoS 1, purely from doubling the network transitions per message and holding state longer on both ends (NotebookLM, MQTT Reliability notebook, 2026).

Packets Per Message: QoS 0 vs QoS 1 vs QoS 2 Each additional packet is a network round trip that adds latency and holds broker state longer QoS 0 1 packet PUBLISH QoS 1 2 packets PUBLISH, PUBACK QoS 2 4 packets PUBLISH, PUBREC, PUBREL, PUBCOMP Source: NotebookLM, MQTT Reliability notebook (single-threaded broker: 2.20ms QoS 0 to 7.93ms QoS 2, a 3.6x penalty)
QoS 2 quadruples the packet count of QoS 0 and doubles QoS 1, and every extra packet is state the broker has to hold until the handshake completes (NotebookLM, MQTT Reliability notebook).

Broker architecture decides how badly that overhead bites. Multi-threaded brokers like EMQX and RabbitMQ process acknowledgments asynchronously and hold QoS 1 and QoS 2 latency near 1.65 to 3.5 milliseconds. Mosquitto, a single-threaded broker, has to serialize message delivery with acknowledgment processing, and its latency climbed from 2.20 milliseconds at QoS 0 to 7.93 milliseconds at QoS 2, a 3.6x penalty (NotebookLM, MQTT Reliability notebook, 2026).

Bandwidth follows the same curve. A small QoS 1 message runs around 341 bytes on the wire; the same message at QoS 2 runs closer to 667 bytes, almost double, once you account for the extra packets and headers (NotebookLM, MQTT Reliability notebook, 2026). Multiply that across a few hundred tags on a metered cellular data plan and the difference stops being academic.

The failure mode most teams miss is that QoS 2's cost isn't fixed. It's conditional on network quality. On a clean LAN, the four-packet handshake completes in a couple of milliseconds and nobody notices. On a lossy cellular link, every dropped packet in that sequence forces the entire handshake to restart, which is exactly how worst-case latency reaches 30 seconds instead of 8 milliseconds (NotebookLM, MQTT Reliability notebook, 2026). QoS 2 doesn't fail gracefully. It fails by getting slower, one retry at a time, until someone notices the dashboard is stale.

Why Does QoS 1 Still Duplicate Messages?

QoS 1 guarantees a message arrives, not that it arrives once, because the acknowledgment loop has two failure points, not one. Either the original PUBLISH is lost on the way to the broker, or the PUBACK confirming it is lost on the way back (NotebookLM, MQTT Reliability notebook, 2026). Both look identical to the sender: silence, followed by a timeout.

When that timeout fires, the sender retransmits the PUBLISH packet with its DUP flag set to 1. If the first PUBLISH actually reached the broker and only the PUBACK went missing, the broker now receives the same message twice and has no protocol-level way to know it's a repeat (NotebookLM, MQTT Reliability notebook, 2026). Deduplication becomes the application's problem, not MQTT's.

QoS 2 closes exactly this gap. The PUBREC and PUBREL packets let both sides agree on whether an incoming PUBLISH is a retransmission or a genuinely new message, using the Packet Identifier as the dividing line (NotebookLM, MQTT Reliability notebook, 2026). Any PUBLISH that arrives before the matching PUBREL is a duplicate by definition; anything after PUBCOMP is new. That's the mechanism, not a side effect, and it's the entire reason QoS 2 costs twice the packets of QoS 1.

I've watched a historian silently double-count a batch completion event because a downstream consumer assumed QoS 1 meant "exactly once" instead of "at least once." The fix wasn't switching to QoS 2 everywhere. It was adding an idempotency key to the payload so the consumer could drop the duplicate itself, at a fraction of QoS 2's overhead.

What Happens When a Device Goes Offline?

A device that drops off the network doesn't lose its queued messages by default, but the broker's behavior depends entirely on the session flags the device set when it connected. Get those flags wrong and you either lose data silently or bloat the broker with messages nobody will ever collect.

Persistent Sessions and Clean Start

Setting Clean Session (MQTT 3.1.1) or Clean Start (MQTT 5.0) to true tells the broker to discard the client's session state on connect, including any subscriptions and previously queued messages. Setting it to false creates a persistent session: the broker keeps the client's subscriptions alive while it's offline and queues new QoS 1 and QoS 2 messages for delivery on reconnect (NotebookLM, MQTT Reliability notebook, 2026).

QoS 0 sits outside this entirely. The broker never queues QoS 0 messages for an offline client, persistent session or not, because QoS 0 was never meant to survive a disconnect in the first place (NotebookLM, MQTT Reliability notebook, 2026). MQTT 5.0 adds a Session Expiry Interval on top of the clean-start flag, letting a client specify exactly how long its session and queued messages should survive after a disconnect, from 0 seconds (discard immediately) up to 0xFFFFFFFF (keep indefinitely).

Retained Messages Aren't Session State

A retained message is a different mechanism from a persistent session, and it's easy to conflate the two. When a publisher sets the retain flag, the broker stores that single message per topic at its original QoS level, and any client that subscribes later, even a brand-new client with no session history, receives it immediately (NotebookLM, MQTT Reliability notebook, 2026).

That solves a specific problem: a new subscriber has no way to know a topic's current value until the next publish, which could be minutes or hours away. A retained message hands it the last known good value on subscribe. Delivery still follows the standard QoS downgrade rule, the effective QoS is whichever is lower, the message's original QoS or the subscriber's requested maximum.

Last Will and Testament

Last Will and Testament, registered at connect time with a topic, payload, QoS, and retain flag, is what tells the network a device is gone rather than just quiet. If the connection drops ungracefully, through a power loss, a network failure, or a missed keepalive, the broker publishes that stored LWT message on the client's behalf (NotebookLM, MQTT Reliability notebook, 2026). A graceful DISCONNECT skips it entirely, since the client is telling the broker on purpose that it's leaving.

QoS 1 is the recommended level for LWT in production, since the offline notification itself needs to arrive reliably (NotebookLM, MQTT Reliability notebook, 2026). Pairing that with a retained flag means a dashboard that connects hours after a device failed still sees the correct offline state the moment it subscribes, rather than a stale "online" reading nobody ever corrected.

Store-and-Forward Buffering at the Edge

An edge gateway that loses its connection to the broker doesn't stop collecting data; it buffers locally. Data routes first to a RAM-based ring buffer, then lazy-flushes to local flash storage, typically SQLite or an embedded time-series store, only when the RAM buffer fills or the outage stretches on, because writing every high-frequency sample straight to flash wears the hardware out early (NotebookLM, MQTT Reliability notebook, 2026).

If an outage runs for weeks and flash storage fills too, the gateway applies triage: FIFO purging of the oldest routine data, priority preservation for equipment alarms, and compression to stretch the remaining space (NotebookLM, MQTT Reliability notebook, 2026). On reconnect, the gateway replays the buffered data sequentially and only deletes its local copy after an application-level acknowledgment, a PUBACK, confirms the broker actually received it. Sparkplug B tags this backfilled data with an is_historical flag and its original timestamp so a receiving historian doesn't mistake old readings for live ones.

Tuning Keepalive Over Cellular

The default 60-second keepalive that ships in most MQTT libraries is often wrong for cellular. Carrier-grade NAT gateways drop idle translation mappings after 30 to 120 seconds of inactivity, and once that mapping is gone, the client's socket goes half-open: it keeps sending into a connection nobody's listening to, and neither TCP nor the application layer notices on its own (NotebookLM, MQTT Reliability notebook, 2026).

A 25-second keepalive clears the strictest 30-second CGNAT timeout with margin, and it's the recommended value if you're seeing silent, unexplained drops on a mobile link. If your carrier runs a more forgiving 5-minute timeout, 250 to 290 seconds keeps the connection alive with less overhead (NotebookLM, MQTT Reliability notebook, 2026). Either way, the broker declares a connection dead and fires the LWT after 1.5 times the negotiated keepalive interval with no traffic.

That interval isn't free. A 25-second keepalive generates roughly 2.4 KB of PINGREQ/PINGRESP overhead per minute, and every ping wakes a cellular radio, which drains battery on a device that's supposed to sleep between readings (NotebookLM, MQTT Reliability notebook, 2026). For a battery-powered sensor that publishes every few minutes, disconnecting and reconnecting for each publish, rather than holding a keepalive open, is usually the better trade: the TCP/TLS handshake costs a few seconds, but it costs far less power than an idle radio pinging a broker between naps.

Citation capsule: A persistent session (Clean Start = false) queues QoS 1 and QoS 2 messages for an offline client but never queues QoS 0; retained messages independently hand new subscribers a topic's last value; Last Will and Testament, ideally at QoS 1 and retained, tells the network the moment a device drops rather than leaving it to guess (NotebookLM, MQTT Reliability notebook, 2026).

How Does Sparkplug B Change the QoS Calculus?

Sparkplug B throws out the QoS-based reliability model almost entirely and mandates QoS 0 for every operational message type, including birth and death certificates, to eliminate the ongoing overhead of acknowledgment handshakes (NotebookLM, MQTT Reliability notebook, 2026). That looks reckless until you see what replaces the guarantee.

Instead of an acknowledgment per message, Sparkplug B tracks two sequence counters. The seq counter increments with every operational message a node sends; if a subscribing host detects a gap in that sequence, it knows a QoS 0 packet was dropped and issues a rebirth command, forcing the node to reinitialize and republish its full state. The bdSeq counter increments once per physical session and exists so a delayed death certificate from an old, already-replaced session can't wrongly mark a healthy current session as dead (NotebookLM, MQTT Reliability notebook, 2026).

The one exception is the Primary Host's STATE message, which runs at QoS 1 and is retained, because host availability has to be signaled reliably to every edge node watching it (NotebookLM, MQTT Reliability notebook, 2026). Everything else stays at QoS 0 and leans on sequence tracking instead of handshakes, which is exactly how Sparkplug B gets its state awareness without paying QoS 2's latency tax. For the full mechanics of birth and death certificates, see our Sparkplug B guide; for how Sparkplug fits alongside plain OPC UA and MQTT in a broader architecture, see OPC UA vs MQTT.

One caveat worth flagging if you're scaling backend consumers: shared subscriptions, which load-balance messages across a pool of subscribing workers, break per-client message ordering by design, since no single worker sees the full sequence. That's a real problem for Sparkplug's seq-based gap detection, so route Sparkplug topics to dedicated, non-shared subscribers instead of a shared subscription group (NotebookLM, MQTT Reliability notebook, 2026).

How Do You Choose QoS Per Data Type?

Match the QoS level to what a lost or duplicated message actually costs you, not to how important the data feels. A vibration trend that updates every second can afford to lose one sample; a safety interlock command cannot.

QoS Level Handshake Guarantee Overhead Duplicate Risk Best Fit
QoS 0 1 packet (PUBLISH) At most once Lowest; sub-ms to 2.5ms typical None, but messages can be silently lost High-frequency telemetry that's quickly superseded: vibration, temperature trends
QoS 1 2 packets (PUBLISH, PUBACK) At least once Moderate; ~341 bytes per small message Possible on lost PUBACK; needs app-level dedup Default for most industrial messaging: state changes, alerts, most sensor data
QoS 2 4 packets (PUBLISH, PUBREC, PUBREL, PUBCOMP) Exactly once Highest; ~667 bytes, up to 50% throughput loss None by design Mission-critical commands: billing events, safety interlock overrides, actuation

Most industrial deployments settle on QoS 1 as the default and reserve QoS 2 for the narrow set of messages where a duplicate would cause real physical or financial harm, not for everything that sounds important (NotebookLM, MQTT Reliability notebook, 2026). A pump station's flow rate belongs at QoS 0 or QoS 1. A remote shutdown command belongs at QoS 2, and nowhere else on the same network needs to pay that price.

If your network still runs Modbus alongside MQTT at the edge, our Modbus RTU vs Modbus TCP guide covers that legacy layer, and our Unified Namespace explainer covers where QoS choices fit once multiple systems share one broker as a single source of truth.

Frequently Asked Questions

What is the difference between MQTT QoS 0, 1, and 2?

QoS 0 fires a message once with no acknowledgment; QoS 1 guarantees delivery but may duplicate; QoS 2 guarantees exactly one delivery through a four-packet handshake. Each step up trades bandwidth and latency for a stronger guarantee (NotebookLM, MQTT Reliability notebook, 2026).

Does QoS 2 guarantee no duplicate messages?

Yes. The PUBREC and PUBREL exchange lets both sides synchronize on a single Packet Identifier, so the receiver never delivers the same message twice, even if a packet in the handshake gets lost and retransmitted (NotebookLM, MQTT Reliability notebook, 2026).

Why do I still see duplicate messages at QoS 1?

QoS 1 only guarantees a PUBACK for every PUBLISH, not a single delivery. If the PUBACK is lost or delayed, the sender retransmits with the DUP flag set, and your application has to handle the duplicate itself (NotebookLM, MQTT Reliability notebook, 2026).

What is a good default QoS for industrial MQTT?

QoS 1 is the default most IIoT deployments settle on. It guarantees delivery at roughly half the bandwidth of QoS 2, and reserving QoS 2 for the small set of messages where a duplicate would cause real harm keeps overhead manageable (NotebookLM, MQTT Reliability notebook, 2026).

Does Sparkplug B use QoS 1 or QoS 2?

Neither, for data. Sparkplug B mandates QoS 0 for every operational message, including birth and death certificates, and relies on sequence numbers to detect gaps. Only the Primary Host STATE message runs at QoS 1 (NotebookLM, MQTT Reliability notebook, 2026).

Conclusion

QoS isn't a dial you turn up for safety. It's a specific trade: every extra packet buys a stronger guarantee and costs latency, bandwidth, and broker state. QoS 0 is cheap and disposable. QoS 1 is the honest default for most industrial telemetry. QoS 2 is expensive and belongs only where a duplicate would genuinely hurt.

The pump station from the opening didn't need faster hardware or a better cellular plan. It needed someone to look at each tag and ask what actually breaks if this message arrives twice, or not at all, then set QoS accordingly instead of defaulting to the strongest guarantee everywhere. Session persistence, retained messages, and Last Will and Testament handle the rest of the reliability story QoS alone can't cover.

Start every new MQTT deployment by sorting tags into "can lose it," "must arrive," and "must arrive exactly once," and let that sorting drive the QoS assignment, not habit. For how these choices change once OPC UA and MQTT sit in the same architecture, revisit OPC UA vs MQTT.

What is the difference between MQTT QoS 0, 1, and 2?
QoS 0 fires a message once with no acknowledgment; QoS 1 guarantees delivery but may duplicate; QoS 2 guarantees exactly one delivery through a four-packet handshake. Each step up trades bandwidth and latency for a stronger guarantee (NotebookLM, MQTT Reliability notebook, 2026).
Does QoS 2 guarantee no duplicate messages?
Yes. The PUBREC and PUBREL exchange lets both sides synchronize on a single Packet Identifier, so the receiver never delivers the same message twice, even if a packet in the handshake gets lost and retransmitted (NotebookLM, MQTT Reliability notebook, 2026).
Why do I still see duplicate messages at QoS 1?
QoS 1 only guarantees a PUBACK for every PUBLISH, not a single delivery. If the PUBACK is lost or delayed, the sender retransmits with the DUP flag set, and your application has to handle the duplicate itself (NotebookLM, MQTT Reliability notebook, 2026).
What is a good default QoS for industrial MQTT?
QoS 1 is the default most IIoT deployments settle on. It guarantees delivery at roughly half the bandwidth of QoS 2, and reserving QoS 2 for the small set of messages where a duplicate would cause real harm keeps overhead manageable (NotebookLM, MQTT Reliability notebook, 2026).
Does Sparkplug B use QoS 1 or QoS 2?
Neither, for data. Sparkplug B mandates QoS 0 for every operational message, including birth and death certificates, and relies on sequence numbers to detect gaps. Only the Primary Host STATE message runs at QoS 1 (NotebookLM, MQTT Reliability notebook, 2026).