Skip to main content

Overview

Quality of Service (QoS) in MQTT defines the guarantee of message delivery between clients and the broker. MQTT provides three QoS levels, each offering different trade-offs between reliability, bandwidth, and latency.
QoS is applied separately for publish (client → broker) and subscribe (broker → client) operations.

QoS Levels Explained

QoS 0: At Most Once

The message is delivered at most once, or not at all. No acknowledgment, no retries, no storage. Characteristics:
  • Fastest, lowest overhead
  • No acknowledgment (fire-and-forget)
  • Messages may be lost if network fails
  • No message duplication
Use cases:
  • High-frequency sensor data where occasional loss is acceptable
  • Temperature readings, GPS coordinates
  • Ambient monitoring where latest value matters most
import paho.mqtt.client as mqtt

client = mqtt.Client("qos0_publisher")
client.connect("localhost", 1883)

# Publish with QoS 0 (default)
client.publish("sensors/temperature", "22.5", qos=0)
print("Message sent (fire and forget)")

client.disconnect()

QoS 1: At Least Once

The message is delivered at least once, guaranteed. May result in duplicates. Characteristics:
  • Acknowledged delivery (PUBACK)
  • Broker stores message until acknowledged
  • Retries if acknowledgment not received
  • May deliver duplicates if PUBACK is lost
Use cases:
  • Important sensor readings
  • Device status updates
  • Command delivery where duplicates can be handled
  • Most general-purpose messaging
import paho.mqtt.client as mqtt
import time

def on_publish(client, userdata, mid):
    print(f"Message {mid} acknowledged by broker")

client = mqtt.Client("qos1_publisher")
client.on_publish = on_publish
client.connect("localhost", 1883)
client.loop_start()

# Publish with QoS 1
result = client.publish("devices/status", "online", qos=1)
result.wait_for_publish()
print(f"Message published, awaiting PUBACK...")

time.sleep(1)
client.loop_stop()
client.disconnect()
QoS 1 can deliver duplicates. Implement idempotent message handlers or use message IDs to detect duplicates.

QoS 2: Exactly Once

The message is delivered exactly once, guaranteed. No loss, no duplicates. Characteristics:
  • Four-way handshake (PUBLISH, PUBREC, PUBREL, PUBCOMP)
  • Highest overhead, slowest delivery
  • Guaranteed exactly-once delivery
  • No duplicates, no loss
Use cases:
  • Financial transactions
  • Critical commands (unlock door, disable alarm)
  • Billing/metering data
  • Any scenario where duplicates or loss are unacceptable
import paho.mqtt.client as mqtt
import time

def on_publish(client, userdata, mid):
    print(f"Message {mid} delivered exactly once")

client = mqtt.Client("qos2_publisher")
client.on_publish = on_publish
client.connect("localhost", 1883)
client.loop_start()

# Publish with QoS 2 - exactly once delivery
result = client.publish(
    "transactions/payment",
    '{"amount": 100, "currency": "USD"}',
    qos=2
)
result.wait_for_publish()
print("Transaction message delivered")

time.sleep(1)
client.loop_stop()
client.disconnect()

QoS Comparison Table

FeatureQoS 0QoS 1QoS 2
Delivery GuaranteeAt most onceAt least onceExactly once
AcknowledgmentNonePUBACKPUBREC/PUBREL/PUBCOMP
RetriesNoYesYes
Can Lose MessagesYesNoNo
Can DuplicateNoYesNo
BandwidthLowestMediumHighest
LatencyLowestMediumHighest
Storage RequiredNoneBroker & ClientBroker & Client

Subscriber QoS

The effective QoS is the minimum of publisher and subscriber QoS levels.
# Publisher sends with QoS 2
publisher.publish("sensors/data", "value", qos=2)

# Subscriber subscribes with QoS 1
subscriber.subscribe("sensors/data", qos=1)

# Effective QoS: 1 (at least once)
# Broker → Subscriber uses QoS 1

Practical Examples

Smart Home Temperature Monitoring

import paho.mqtt.client as mqtt
import time
import random

client = mqtt.Client("temp_sensor")
client.connect("localhost", 1883)
client.loop_start()

try:
    while True:
        temp = 20 + random.uniform(-2, 2)
        
        # Use QoS 0 for frequent updates
        client.publish(
            "home/livingroom/temperature",
            f"{temp:.2f}",
            qos=0
        )
        
        # Use QoS 1 for threshold alerts
        if temp > 25:
            client.publish(
                "home/alerts/high_temp",
                f"Temperature alert: {temp:.2f}°C",
                qos=1
            )
        
        time.sleep(10)
except KeyboardInterrupt:
    client.loop_stop()
    client.disconnect()

Industrial Control System

import paho.mqtt.client as mqtt
import json

def send_critical_command(client, machine_id, command):
    """Send critical command with QoS 2"""
    payload = json.dumps({
        "machine_id": machine_id,
        "command": command,
        "timestamp": time.time()
    })
    
    result = client.publish(
        f"factory/machine/{machine_id}/command",
        payload,
        qos=2,  # Exactly once for critical commands
        retain=False
    )
    result.wait_for_publish()
    print(f"Command '{command}' delivered to {machine_id}")

def send_telemetry(client, machine_id, data):
    """Send telemetry with QoS 0"""
    client.publish(
        f"factory/machine/{machine_id}/telemetry",
        json.dumps(data),
        qos=0  # Fire-and-forget for high-frequency data
    )

client = mqtt.Client("factory_controller")
client.connect("localhost", 1883)
client.loop_start()

# High-frequency telemetry (QoS 0)
send_telemetry(client, "m001", {"rpm": 1500, "temp": 45})

# Critical emergency stop (QoS 2)
send_critical_command(client, "m001", "EMERGENCY_STOP")

client.loop_stop()
client.disconnect()

Handling QoS in Subscriptions

Duplicate Detection (QoS 1)

import paho.mqtt.client as mqtt

# Track processed message IDs to detect duplicates
processed_messages = set()

def on_message(client, userdata, msg):
    # Create message fingerprint
    msg_id = f"{msg.topic}:{msg.payload.decode()}"
    
    if msg_id in processed_messages:
        print(f"Duplicate detected: {msg.topic}")
        return
    
    processed_messages.add(msg_id)
    print(f"Processing: {msg.topic} = {msg.payload.decode()}")
    
    # Keep set size manageable
    if len(processed_messages) > 1000:
        processed_messages.clear()

client = mqtt.Client("dedup_subscriber")
client.on_message = on_message
client.connect("localhost", 1883)
client.subscribe("sensors/#", qos=1)
client.loop_forever()

Performance Considerations

Choosing the Right QoS:
  1. Use QoS 0 when:
    • High message frequency (>1/second)
    • Latest value is most important
    • Network is reliable
    • Example: Live sensor readings
  2. Use QoS 1 when:
    • Messages must arrive
    • Duplicates can be handled
    • Balance of reliability and performance needed
    • Example: Status updates, notifications
  3. Use QoS 2 when:
    • Exactly-once delivery is critical
    • Duplicates would cause problems
    • Performance is secondary to correctness
    • Example: Financial data, critical commands

Bandwidth Impact

import time
import paho.mqtt.client as mqtt

def benchmark_qos(qos_level, message_count=100):
    client = mqtt.Client(f"benchmark_qos{qos_level}")
    client.connect("localhost", 1883)
    client.loop_start()
    
    start = time.time()
    
    for i in range(message_count):
        result = client.publish(
            "benchmark/test",
            f"message_{i}",
            qos=qos_level
        )
        if qos_level > 0:
            result.wait_for_publish()
    
    elapsed = time.time() - start
    client.loop_stop()
    client.disconnect()
    
    print(f"QoS {qos_level}: {elapsed:.2f}s for {message_count} messages")
    print(f"Average: {(elapsed/message_count)*1000:.2f}ms per message")

for qos in [0, 1, 2]:
    benchmark_qos(qos)
    print()

Common Pitfalls

Avoid These Mistakes:
  1. Using QoS 2 everywhere: Unnecessary overhead for most use cases
  2. Ignoring duplicates with QoS 1: Can lead to incorrect state
  3. Not waiting for publish acknowledgment: Messages may be lost on disconnect
  4. Mismatching QoS expectations: Publisher QoS 2 doesn’t guarantee subscriber receives QoS 2

Testing QoS Behavior

# Terminal 1: Subscribe with different QoS levels
mosquitto_sub -h localhost -t "test/qos0" -q 0 -v
mosquitto_sub -h localhost -t "test/qos1" -q 1 -v
mosquitto_sub -h localhost -t "test/qos2" -q 2 -v

# Terminal 2: Publish with different QoS
mosquitto_pub -h localhost -t "test/qos0" -m "QoS 0 message" -q 0
mosquitto_pub -h localhost -t "test/qos1" -m "QoS 1 message" -q 1
mosquitto_pub -h localhost -t "test/qos2" -m "QoS 2 message" -q 2

# Test network interruption
# Stop subscriber, publish QoS 1/2 messages, restart subscriber
# QoS 0: Lost
# QoS 1/2: Delivered after reconnection (if clean_session=False)

Next Steps

Retained Messages

Learn how retained messages work with QoS

Publishing & Subscribing

Master the pub/sub pattern

Build docs developers (and LLMs) love