← Back to blog
ARCHITECTURE7 min read

QoS 0, 1, and 2: Choosing the Right Delivery Guarantee for Every Message

HB
Henco Burger
March 31, 2026

Not all messages are equal. A GPS coordinate from a moving vehicle that arrives 200ms late is still useful. That same coordinate arriving twice is harmless. But a payment confirmation that gets processed twice is a serious problem, and a payment that disappears silently is even worse.

Quality of Service (QoS) levels let you express how important it is that a specific message gets delivered, and how much overhead you are willing to pay to guarantee it. The three levels originated in MQTT but the concepts apply broadly to any real-time messaging system.

QoS 0: At Most Once (Fire and Forget)

QoS 0 is the simplest delivery mode. The publisher sends the message once. The broker delivers it if the subscriber is currently connected. No acknowledgment, no retry, no persistence. If the connection drops between publish and delivery, the message is gone.

// QoS 0: no ack, no retry
await topic.publish(
  { lat: 51.5074, lng: -0.1278, speed: 42 },
  { qos: 0 }
)

// The broker sends this to subscribers and forgets it.
// If a subscriber is offline, they miss it. That's acceptable
// for position updates because the next one arrives in 100ms.

QoS 0 has the lowest latency and the lowest broker overhead. There is no acknowledgment round-trip, no message storage, no retry queue. The broker processes the message and moves on.

When to use QoS 0

  • High-frequency telemetry. A temperature sensor sending a reading every second does not need guaranteed delivery. The next reading arrives momentarily. Losing one sample in a thousand is acceptable.
  • Typing indicators. Whether or not "Alice is typing..." arrives is not important. It is a transient UI hint. A missed indicator does not corrupt state.
  • Live cursor positions. In a collaborative document editor, cursor positions are sent many times per second. Missing one frame is invisible to users.
  • Animated dashboards. A dashboard showing a live metric that updates 10 times per second can afford to drop occasional frames without the user noticing.

QoS 1: At Least Once (Acknowledged Delivery)

QoS 1 adds an acknowledgment handshake. The publisher sends a message and waits for an ack from the broker. If no ack arrives within the timeout window, the publisher resends the message. This continues until the broker acknowledges receipt.

// QoS 1: publisher retries until ack received
await topic.publish(
  { type: 'message', text: 'Hey, are you free?', msgId: 'msg_01' },
  { qos: 1 }
)

// Flow:
// 1. Publisher sends MESSAGE (msgId: msg_01)
// 2. Broker stores message, delivers to subscribers
// 3. Broker sends PUBACK to publisher
// 4. Publisher discards the message from retry queue
//
// If step 3 fails: publisher resends MESSAGE
// Subscriber may receive it twice - application must handle this

The "at least once" label is precise: the message will definitely be delivered, but it might be delivered more than once if an ack gets lost and the publisher retries. The subscriber must handle duplicate delivery, either by ignoring duplicates (idempotent processing) or by tracking message IDs and deduplicating.

When to use QoS 1

  • Chat messages. A message that never arrives is a broken product. A message that appears twice is an edge case the UI can handle by deduplicating on message ID. QoS 1 is the right default for chat.
  • Notifications. A push notification that gets lost means the user never knew about an important event. Receiving it twice is an annoyance. The reliability side of that tradeoff wins.
  • Order status updates. A user waiting for their delivery status to change needs that update. Missing it means a support ticket. Receiving it twice means a brief flicker in the UI.
  • IoT commands. Sending a "turn off" command to a device that occasionally drops packets needs retry logic. A double-delivery of "turn off" when the device is already off is harmless.

QoS 2: Exactly Once (Four-Way Handshake)

QoS 2 guarantees that a message is delivered exactly once. No drops, no duplicates. It achieves this with a four-step handshake between publisher, broker, and subscriber.

// QoS 2: exactly-once delivery via four-way handshake
await topic.publish(
  { type: 'payment', amount: 299.00, orderId: 'ord_7x2k' },
  { qos: 2 }
)

// Full flow:
// 1. Publisher sends PUBLISH (stored by broker)
// 2. Broker sends PUBREC (publish received)
// 3. Publisher sends PUBREL (publish release) - broker can now deliver
// 4. Broker delivers to subscriber, sends PUBCOMP (complete)
// 5. Publisher removes from retry queue
//
// At no point in this flow can a retry produce a duplicate delivery.
// The broker tracks message state across all four steps.

This four-step protocol is significantly more expensive than QoS 1. Each message requires two round-trips instead of one. The broker must persist the message state across the full handshake, which means more writes, more memory, and more latency. For a high-volume stream this overhead is prohibitive.

When to use QoS 2

  • Financial transactions. A payment that processes twice charges the customer twice. A payment that is silently dropped causes a lost sale or a failed service. Exactly-once delivery is a hard requirement here.
  • Inventory mutations. Decrementing stock by one must happen exactly once per sale. A retry that gets delivered twice would decrement stock by two.
  • Audit log entries. If you are building a compliance system where every event must appear exactly once in the audit trail, QoS 2 ensures the record is accurate.
  • Smart contracts or on-chain triggers. Any message that triggers an irreversible action downstream needs exactly-once delivery.

The Latency vs Reliability Tradeoff

The cost of each QoS level is concrete:

QoS Level   Round-Trips   Broker Storage   Relative Latency   Duplicate Risk
--------------------------------------------------------------------------
QoS 0           0            None               1x                None
QoS 1           1            Until ack          1.5-2x            Possible
QoS 2           2            Until complete     2.5-4x            None

For most interactive apps, QoS 0 and QoS 1 cover everything. QoS 0 for ephemeral state (presence, cursors, typing) and QoS 1 for durable events (messages, notifications). QoS 2 is reserved for cases where the downstream consequence of a duplicate or a drop is financially or legally significant.

Per-Message QoS, Not Per-Connection

One of the design decisions that matters most in practice is whether QoS is a per-connection setting or a per-message setting. If it is per-connection, every message on that connection uses the same delivery guarantee. That forces you to open multiple connections if you have mixed requirements, or to use the most conservative QoS for everything (which means paying the QoS 2 cost for typing indicators that do not need it).

Per-message QoS is the right model. A single connection can carry QoS 0 telemetry, QoS 1 chat messages, and QoS 2 payment confirmations simultaneously. The broker applies the appropriate delivery logic per message based on what the publisher requested.

How NoLag Handles QoS

NoLag attaches QoS per message, not per connection. When you publish, you specify the delivery guarantee for that specific message. The SDK and broker handle the ack handshake, retry logic, and deduplication transparently based on the level you specify.

// Ephemeral presence update - no storage needed
await topic.publish(presenceUpdate, { qos: 0 })

// Chat message - must arrive, duplicates are manageable
await topic.publish(chatMessage, { qos: 1 })

// Payment event - must arrive exactly once
await topic.publish(paymentEvent, { qos: 2 })

All three can flow over the same WebSocket connection. The connection overhead is paid once. The per-message overhead reflects only what that specific message requires.

Practical Guidance: What to Use When

If you are not sure where to start, here is a simple decision tree:

  • The message is transient state (typing, cursor, presence). Use QoS 0. A missed delivery has no lasting effect.
  • The message represents a durable event (chat, notification, status change). Use QoS 1. Ensure your subscriber deduplicates on message ID for resilience.
  • The message triggers an irreversible action (payment, inventory decrement, audit record). Use QoS 2. Pay the latency cost. It is worth it.

The goal is to match the delivery guarantee to the actual consequence of failure. Over-engineering everything to QoS 2 wastes resources and adds latency. Under-engineering payment flows to QoS 0 is a production incident waiting to happen. The right answer is almost always a mix, applied per message type.