MQTT QoS 0, 1, and 2 Explained: Choosing the Right Level for IoT
Quality of Service in MQTT is one of those concepts that every IoT developer knows exists and almost nobody fully understands until they hit a production problem. This guide explains the protocol mechanics, not just the labels — so you can make the right choice for each message type in your system.
What QoS Actually Controls
MQTT QoS governs the delivery guarantee between a *publisher* and the *broker*, and separately between the *broker* and each *subscriber*. It does NOT provide end-to-end delivery guarantees — QoS 1 from device to broker doesn't help you if the broker drops the message before delivering to the subscriber.
The three levels trade off protocol overhead (extra round-trips, stored message state) for delivery assurance.
QoS 0: Fire-and-Forget
The simplest path: publisher sends one PUBLISH packet, no acknowledgement, no retransmission. If the network drops the packet, it's gone.
Publisher → [PUBLISH] → Broker → [PUBLISH] → Subscriber
(delivered or lost, no feedback)
Protocol overhead: 2-byte fixed header + variable header. Zero state stored at broker or client.
Battery impact: Minimum possible — one radio transmission per message.
// ESP32: QoS 0 publish (PubSubClient default)
void publishTelemetry(float temperature) {
char payload[64];
snprintf(payload, sizeof(payload),
"{"temp":%.2f,"ts":%lu}", temperature, millis()); // Third argument: retain flag. Fourth: QoS (0 = fire-and-forget)
mqttClient.publish("devices/" DEVICE_ID "/telemetry", payload, false);
// PubSubClient defaults to QoS 0
}
Use QoS 0 for:
Do NOT use QoS 0 for:
QoS 1: At-Least-Once Delivery
The publisher stores the message and transmits a PUBLISH packet. The broker must respond with PUBACK. If PUBACK isn't received within the timeout, the publisher retransmits (with DUP flag set) until acknowledged.
Publisher → [PUBLISH packetId=42] → Broker
Publisher ← [PUBACK packetId=42] ← Broker
(Broker delivers to subscribers, which may also PUBACK independently)
Protocol overhead: One extra round-trip per message. Broker stores message in session until all subscribers acknowledge.
The duplicate problem: If the broker receives the PUBLISH and sends PUBACK, but PUBACK is lost in transit, the publisher retransmits. The broker may deliver the message twice to subscribers. Your subscriber code must handle duplicates:
// Node.js: idempotent QoS 1 subscriber with deduplication
const seen = new Map() // messageId → timestamp
const DEDUP_WINDOW_MS = 30000mqttClient.on('message', (topic, buffer, packet) => {
const messageId = ${packet.messageId}-${topic}
// Deduplicate
if (seen.has(messageId)) return
seen.set(messageId, Date.now())
// Clean up dedup window periodically
if (seen.size > 10000) {
const cutoff = Date.now() - DEDUP_WINDOW_MS
seen.forEach((ts, id) => { if (ts < cutoff) seen.delete(id) })
}
processMessage(topic, JSON.parse(buffer))
})
Use QoS 1 for:
QoS 2: Exactly-Once Delivery
The most complex level — a four-message handshake ensures the message is delivered exactly once, with no duplicates.
Publisher → [PUBLISH packetId=42] → Broker (store message)
Publisher ← [PUBREC packetId=42] ← Broker (received and stored)
Publisher → [PUBREL packetId=42] → Broker (release: safe to deliver)
Publisher ← [PUBCOMP packetId=42] ← Broker (complete: discard stored msg)
(Broker now delivers to subscribers with QoS 2 handshake)
Protocol overhead: Four round-trips per message. Both publisher and broker must persist state across potential connection drops. Subscriber must also maintain state for the second handshake.
Battery impact: Significantly higher — four radio transmissions plus state writes to flash (if persisting across reboots).
Throughput impact: On AWS IoT Core, QoS 2 message rate is subject to tighter throttle limits than QoS 0/1. At scale, QoS 2 can become a bottleneck.
// Node.js: publish at QoS 2 for billing data
client.publish(
devices/${deviceId}/billing/consumption,
JSON.stringify({ kWh: reading.kWh, periodEnd: reading.ts }),
{ qos: 2 },
(err) => {
if (err) {
// QoS 2 publish failed after all retries — escalate
alertOpsTeam(Billing message undelivered for ${deviceId})
}
}
)
Use QoS 2 for:
Avoid QoS 2 for:
Overhead Comparison Table
| QoS | Messages per publish | Bytes overhead | State at broker | State at client | |-----|---------------------|----------------|-----------------|-----------------| | 0 | 1 | ~2 bytes | None | None | | 1 | 2 (+ retries if needed) | ~4 bytes × 2 | Until PUBACK | Until PUBACK | | 2 | 4 | ~4 bytes × 4 | Until PUBCOMP | Until PUBCOMP |
At 1,000 messages/second, QoS 2 generates 4,000 broker-level operations versus 1,000 for QoS 0. The broker cost difference is real: AWS IoT Core charges per message, and a four-message QoS 2 exchange counts as four messages.
Persistent Sessions and QoS Interaction
QoS 1 and 2 only queue messages for offline subscribers if you use persistent sessions (cleanSession: false on the MQTT connection). With a clean session (the default), the broker discards QoS 1+ messages for any disconnected subscriber.
For device command delivery, always use persistent sessions:
// Device connects with persistent session
const client = mqtt.connect(brokerUrl, {
clientId: deviceId,
clean: false, // persistent session
reconnectPeriod: 5000,
})// Commands queued while device is offline will be delivered on reconnect
client.subscribe(devices/${deviceId}/commands, { qos: 1 })
Decision Matrix
| Message Type | Recommended QoS | Reason | |-------------|----------------|--------| | Sensor telemetry (frequent) | 0 | Loss acceptable, next reading coming | | Sensor telemetry (infrequent, >5min) | 1 | Loss would create data gaps | | Actuator command | 1 + dedup | Must arrive, duplicates manageable | | Configuration update | 1 | Must arrive, idempotent | | Billing / metering | 2 | Exactly-once required, low rate | | OTA firmware chunk | 1 | HTTP OTA is better; use 1 if MQTT-based | | Heartbeat / keepalive | 0 | Loss is fine — broker has LWT |
For the broader MQTT vs HTTP decision, see [MQTT vs HTTP for IoT](/blog/mqtt-vs-http-iot-protocol-comparison). For how QoS interacts with fleet-scale message routing on AWS, see [IoT Fleet Management with AWS IoT Core](/blog/iot-fleet-management-aws-iot-core).
Need help with IoT protocol design? [Contact Code Caracal](/contact) — we've shipped these systems for clients across 15+ countries.