QoS levels
Understanding MQTT Quality of Service levels and when to use each.
QoS determines the delivery guarantee for MQTT messages. Use this guide to pick the right level for each topic, balancing reliability against speed and overhead.
Overview
MQTT defines three QoS levels:
| Level | Name | Guarantee |
|---|---|---|
| 0 | At most once | Fire and forget, may be lost |
| 1 | At least once | Guaranteed delivery, may be duplicated |
| 2 | Exactly once | Guaranteed single delivery |
QoS 0: at most once
The simplest level. The message is sent once with no acknowledgment.
Characteristics
| Property | Value |
|---|---|
| Speed | Fastest, lowest overhead |
| Retry | None |
| Loss | Possible if network issues occur |
| Duplicates | None |
When to use QoS 0
| Scenario | Why it fits |
|---|---|
| Frequent state samples | Missing one is acceptable |
| High-frequency data | Next reading replaces the last |
| Non-critical status updates | Loss is recoverable |
| Severely limited bandwidth | Smallest packet count |
// Heartbeat every second - missing one is fine
client.publish('agents/agent-001/heartbeat', 'alive', { qos: 0 });QoS 1: at least once
The message is retried until acknowledged. Most common choice.
Publisher -> Broker: PUBLISH
Publisher <- Broker: PUBACK
Broker -> Subscriber: PUBLISH
Broker <- Subscriber: PUBACKCharacteristics
| Property | Value |
|---|---|
| Storage | Stored and retried until acknowledged |
| Delivery | Guaranteed |
| Duplicates | Possible if acknowledgment is lost |
| Overhead | Moderate |
When to use QoS 1
| Scenario | Why it fits |
|---|---|
| Important data that must arrive | Retry until acknowledged |
| Commands safely repeatable | Duplicate is fine |
| Events where duplicates are acceptable | Idempotent handlers cover it |
| Most agent state and chat messages | Reliability without QoS 2 cost |
// Agent status update - must arrive, duplicate is OK
client.publish('agents/agent-001/state', 'online', { qos: 1 });QoS 1 is the recommended default for most applications. It provides reliability without the complexity of QoS 2.
QoS 2: exactly once
The highest guarantee. A four-step handshake ensures single delivery.
Publisher -> Broker: PUBLISH
Publisher <- Broker: PUBREC (received)
Publisher -> Broker: PUBREL (release)
Publisher <- Broker: PUBCOMP (complete)
Same 4-step process: Broker -> SubscriberCharacteristics
| Property | Value |
|---|---|
| Delivery | Guaranteed exactly once |
| Duplicates | None |
| Loss | None |
| Overhead | Highest (4 packets vs 1-2) |
| Speed | Slowest |
When to use QoS 2
| Scenario | Why it fits |
|---|---|
| Financial transactions | Duplicates are unacceptable |
| Billing events | Single accounting entry |
| Critical commands | Must not duplicate |
| Any non-idempotent action | Replay would corrupt state |
// Payment notification - must happen exactly once
client.publish('billing/charges', chargeData, { qos: 2 });QoS 2 has significant overhead. Only use it when duplicates would cause real problems.
Comparison
| Aspect | QoS 0 | QoS 1 | QoS 2 |
|---|---|---|---|
| Packets per message | 1 | 2 | 4 |
| Storage required | None | Until PUBACK | Until PUBCOMP |
| Retries | No | Yes | Yes |
| Duplicates possible | No | Yes | No |
| Message loss possible | Yes | No | No |
| Relative speed | Fastest | Medium | Slowest |
QoS downgrade
QoS is negotiated between publisher and subscriber. The broker delivers at the minimum of:
- Publisher's QoS
- Subscriber's QoS
Publisher sends QoS 2
Subscriber subscribed with QoS 1
-> Subscriber receives at QoS 1This means:
Publish QoS 2 + Subscribe QoS 0 = Delivered QoS 0
Publish QoS 1 + Subscribe QoS 2 = Delivered QoS 1
Publish QoS 0 + Subscribe QoS 2 = Delivered QoS 0Code examples
JavaScript (mqtt.js)
const mqtt = require('mqtt');
const client = mqtt.connect('mqtts://mqtt.cloudsignal.app:8883', {
username: 'user@org-id',
password: 'password'
});
// QoS 0 - frequent, non-critical
client.publish('agents/agent-001/heartbeat', 'alive', { qos: 0 });
// QoS 1 - important, duplicates OK
client.publish('agents/agent-001/state', 'online', { qos: 1 });
// QoS 2 - critical, no duplicates
client.publish('billing/event', JSON.stringify(event), { qos: 2 });
// Subscribe with QoS
client.subscribe('commands/#', { qos: 1 });Python (paho-mqtt)
import paho.mqtt.client as mqtt
client = mqtt.Client()
client.username_pw_set('user@org-id', 'password')
client.tls_set()
client.connect('mqtt.cloudsignal.app', 8883)
# QoS 0
client.publish('agents/agent-001/heartbeat', 'alive', qos=0)
# QoS 1
client.publish('agents/agent-001/state', 'online', qos=1)
# QoS 2
client.publish('billing/event', event_json, qos=2)
# Subscribe with QoS
client.subscribe('commands/#', qos=1)Handling duplicates (QoS 1)
When using QoS 1, design your system to handle duplicates.
Idempotent operations
Make operations safe to repeat:
// Good: setting state is idempotent
client.publish('lights/kitchen/state', 'on', { qos: 1 });
// Receiving "on" twice still results in light being on
// Risky: increment operation
client.publish('counter/increment', '1', { qos: 1 });
// Receiving twice would increment twiceDeduplication with message IDs
Include a unique ID and deduplicate on receive:
// Publisher
const messageId = uuid();
client.publish('events/user-action', JSON.stringify({
id: messageId,
action: 'purchase',
amount: 100
}), { qos: 1 });
// Subscriber
const processedIds = new Set();
client.on('message', (topic, payload) => {
const message = JSON.parse(payload);
if (processedIds.has(message.id)) {
return; // Already processed, skip
}
processedIds.add(message.id);
processEvent(message);
});CloudSignal recommendations
| Use case | Recommended QoS |
|---|---|
| Agent state (frequent) | 0 or 1 |
| Agent status updates | 1 |
| Commands to agents | 1 |
| User notifications | 1 |
| Chat messages | 1 |
| Financial/billing events | 2 |
| Critical alerts | 1 or 2 |
Next steps
- Retained messages - Persist last value
- Topics & wildcards - Topic design
- First connection - Try different QoS levels