MQTT Fundamentals

MQTT QoS Levels Explained: QoS 0, 1 and 2 with Packet Flows and Examples

A practical guide to MQTT QoS: how QoS 0, 1 and 2 work on the wire, why the delivered QoS can be lower than you asked for, how sessions queue messages and which level to choose for each use case.

Diagram of MQTT QoS 0, 1 and 2 packet flows between a client and an MQTT broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

MQTT QoS (Quality of Service) is the delivery guarantee a client asks for when it publishes or subscribes. In this guide you will see exactly which packets QoS 0, 1 and 2 exchange, why the QoS that reaches a subscriber can be lower than the one you published with, how sessions queue messages for offline clients, and how to test all of it in Python against broker.connectmq.com.

If MQTT itself is new to you, start with What Is an MQTT Broker? and come back here.

What MQTT QoS Actually Guarantees

MQTT runs on TCP, so bytes are not lost or reordered within a single connection. QoS is about something else: what happens when a connection drops in the middle of a delivery, and whether the receiver might get the same message twice.

Two points are often misunderstood:

  • QoS is hop-by-hop, not end-to-end. A publisher's QoS governs the leg from publisher to broker. The subscription QoS governs the leg from broker to subscriber. The broker is the middleman for both.
  • Higher QoS is not "better" by default. Every level adds packets, state and latency. The right choice depends on what a lost or duplicated message costs you.
Level Name Guarantee Packets per message (one leg) Duplicates possible?
QoS 0 At most once Best effort, no acknowledgement 1 No, but loss is possible
QoS 1 At least once Delivered, may be repeated 2 Yes
QoS 2 Exactly once Delivered once, no duplicates 4 No

QoS 0: At Most Once

QoS 0 is fire-and-forget. The sender writes a PUBLISH packet to the socket and immediately forgets about it. There is no packet identifier and no acknowledgement.

Publisher                     Broker
    |                            |
    |------ PUBLISH (QoS 0) ---->|
    |                            |   (no reply, nothing stored)

If the TCP connection breaks while the packet is still in an OS buffer, the message is gone and nobody knows. That sounds bad, but for a temperature reading sent every two seconds, the next reading replaces the lost one anyway. QoS 0 gives you the lowest latency and the least overhead on constrained devices.

QoS 1: At Least Once

With QoS 1 the sender assigns a packet identifier and keeps the message until the receiver answers with PUBACK carrying the same identifier.

Publisher                     Broker
    |                            |
    |-- PUBLISH (QoS 1, id=7) -->|   broker stores and forwards
    |<------- PUBACK (id=7) -----|
    |                            |   publisher deletes message 7

If the PUBACK never arrives (for example, the connection drops), the sender resends the PUBLISH with the DUP flag set after reconnecting. The catch: the receiver may already have processed the first copy, so it sees the message twice. That is why QoS 1 means at least once.

QoS 1 is the workhorse of most IoT systems. Make your consumers idempotent (for example, include a message ID or timestamp in the payload and ignore repeats) and QoS 1 is usually all you need.

QoS 2: Exactly Once

QoS 2 uses a four-step handshake so both sides agree that the message was delivered exactly one time.

Publisher                       Broker
    |                              |
    |--- PUBLISH (QoS 2, id=9) --->|   broker stores id 9, does not forward twice
    |<------- PUBREC (id=9) -------|   "received"
    |-------- PUBREL (id=9) ------>|   "release it, I will not resend"
    |<------- PUBCOMP (id=9) ------|   "complete", both sides drop id 9

What each step does:

  1. PUBLISH: the sender stores the message and sends it with a packet identifier.
  2. PUBREC: the receiver records the identifier. If a duplicate PUBLISH with the same ID arrives, it is recognised and not delivered again.
  3. PUBREL: the sender discards the message and from now on only remembers the identifier.
  4. PUBCOMP: the receiver forgets the identifier. The exchange is finished.

The cost is two round trips per message and state held on both sides until the flow completes. Use QoS 2 when a duplicate would cause real harm and you cannot make the consumer idempotent, such as a command that increments a counter or triggers a billing event.

MQTT QoS Downgrade: Why You Might Not Get the QoS You Asked For

The QoS a subscriber receives is not simply the QoS the publisher used. Three numbers interact.

Effective QoS = min(publish QoS, subscription QoS)

When the broker forwards a message, it uses the lower of the QoS in the original PUBLISH and the QoS granted for the subscription.

Publisher QoS Subscription QoS Delivered to subscriber
0 2 0
1 2 1
2 1 1
2 0 0
2 2 2

So if you need exactly-once delivery end to end, both the publisher and every subscriber must use QoS 2. A single QoS 0 subscription quietly turns that subscriber's stream into best effort.

The broker's Maximum QoS (MQTT 5)

In MQTT 5 the broker can announce a Maximum QoS in its CONNACK packet. If this property is absent, the client may assume QoS 2 is supported. If it is present, the client must not publish above that level, and the broker will grant subscriptions at no more than that level (the SUBACK tells you the granted QoS).

On ConnectMQ this depends on where you connect:

Environment Maximum QoS
Public test area on broker.connectmq.com QoS 2
Serverless (Free) plan QoS 1
Serverless (Paid) plan QoS 2
Dedicated, BYOC, Self Managed Enterprise QoS 2

Always check the granted QoS in SUBACK instead of assuming. Most client libraries expose it in the subscribe callback. The MQTT 5 specification at docs.oasis-open.org describes the exact rules. For a tour of other CONNACK properties, see MQTT 5 Features.

Sessions, Session Expiry and Offline Queuing

QoS only protects messages across a disconnect if the session survives the disconnect. The session is the state the broker keeps for a client: its subscriptions, in-flight QoS 1/2 messages and messages queued while it is offline.

  • MQTT 3.1.1 uses the Clean Session flag. cleanSession=true throws the session away on disconnect. cleanSession=false asks the broker to keep it.
  • MQTT 5 splits this into Clean Start (start fresh on connect or not) and Session Expiry Interval (how many seconds the session lives after disconnect).

While a persistent session exists and the client is offline, the broker queues QoS 1 and QoS 2 messages that match its subscriptions. QoS 0 messages are not queued for offline clients. When the client reconnects with the same client ID and without a clean start, the queued messages are delivered.

Subscriber (id=sensor-reader)           Broker
    |-- CONNECT clean_start=0, expiry=600 -->|
    |-- SUBSCRIBE plant/# QoS 1 ------------>|
    |  ... connection lost ...               |
    |                                         |  PUBLISH plant/a QoS 1  -> queued
    |                                         |  PUBLISH plant/b QoS 0  -> dropped
    |-- CONNECT clean_start=0 --------------->|
    |<-- CONNACK session_present=1 -----------|
    |<-- PUBLISH plant/a ---------------------|

On ConnectMQ, session expiry is capped per plan, and in the public test area sessions live for up to 30 minutes. Also note that sessions are held in memory: they do not survive a broker restart, so design critical flows to tolerate a reconnect with session_present=0.

How to Choose the Right MQTT QoS Level

Use case Recommended QoS Why
High-frequency telemetry (temperature, vibration) 0 The next sample replaces a lost one; lowest overhead
Device status, alarms, events 1 Must arrive; duplicates are easy to ignore
Commands to actuators (idempotent: "set valve to 40%") 1 Repeating the same command is harmless
Commands that are not idempotent ("increment counter", "dispense") 2 A duplicate would change the outcome
Billing, metering or transaction events 2 (or 1 with dedup IDs) Exactly-once accounting

A practical rule: default to QoS 1 with idempotent consumers, drop to QoS 0 for high-rate data you can afford to lose, and reserve QoS 2 for the few message types where a duplicate is a bug.

Hands-On: MQTT QoS 1 and QoS 2 in Python

The following script uses paho-mqtt 2.x and the public area of broker.connectmq.com (no username or password). It subscribes with QoS 2, publishes one message with QoS 1 and one with QoS 2, and logs the acknowledgements. Install the library with pip install "paho-mqtt>=2.0".

import time
import uuid
import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
UID = uuid.uuid4().hex[:8]
TOPIC = f"connectmq/demo/{UID}/qos"

def on_connect(client, userdata, flags, reason_code, properties):
    print("CONNACK:", reason_code)
    if properties is not None and hasattr(properties, "MaximumQoS"):
        print("Broker Maximum QoS:", properties.MaximumQoS)
    client.subscribe(f"{TOPIC}/#", qos=2)

def on_subscribe(client, userdata, mid, reason_code_list, properties):
    print("SUBACK granted:", [rc.value for rc in reason_code_list])

def on_message(client, userdata, msg):
    print(f"Received {msg.topic} QoS={msg.qos} payload={msg.payload.decode()}")

def on_publish(client, userdata, mid, reason_code, properties):
    print(f"Publish flow complete for mid={mid}")

client = mqtt.Client(
    mqtt.CallbackAPIVersion.VERSION2,
    client_id=f"qos-demo-{UID}",
    protocol=mqtt.MQTTv5,
)
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_message = on_message
client.on_publish = on_publish

client.connect(BROKER, 1883, keepalive=30)
client.loop_start()
time.sleep(2)

client.publish(f"{TOPIC}/one", "hello at least once", qos=1).wait_for_publish()
client.publish(f"{TOPIC}/two", "hello exactly once", qos=2).wait_for_publish()

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

For QoS 1, on_publish fires when PUBACK arrives. For QoS 2, it fires only after PUBCOMP, at the end of the four-step handshake. If no Maximum QoS line is printed, the broker did not send the property, which means QoS 2 is allowed.

Watch the packets with the Mosquitto CLI

The Eclipse Mosquitto command-line tools print every packet with -d, which makes the flows above visible. In one terminal:

mosquitto_sub -h broker.connectmq.com -p 1883 -i qos-sub-a1b2 -t "connectmq/demo/a1b2/#" -q 2 -v -d

In a second terminal, publish with QoS 1 and then QoS 2:

mosquitto_pub -h broker.connectmq.com -p 1883 -i qos-pub-a1b2 -t "connectmq/demo/a1b2/q1" -q 1 -m "hello1" -d
mosquitto_pub -h broker.connectmq.com -p 1883 -i qos-pub-a1b2 -t "connectmq/demo/a1b2/q2" -q 2 -m "hello2" -d

The publisher output shows the exact sequences described earlier:

Client qos-pub-a1b2 sending PUBLISH (d0, q1, r0, m1, 'connectmq/demo/a1b2/q1', ... (6 bytes))
Client qos-pub-a1b2 received PUBACK (Mid: 1, RC:0)
...
Client qos-pub-a1b2 sending PUBLISH (d0, q2, r0, m1, 'connectmq/demo/a1b2/q2', ... (6 bytes))
Client qos-pub-a1b2 received PUBREC (Mid: 1)
Client qos-pub-a1b2 sending PUBREL (m1)
Client qos-pub-a1b2 received PUBCOMP (Mid: 1, RC:0)

Replace a1b2 with your own random string. Everything in the public area is readable by anyone, so never publish sensitive data there. To experiment with downgrades, subscribe with -q 0 and publish with -q 2: the subscriber's debug output will show q0 on the delivered PUBLISH. More public-broker tips are in Free Public MQTT Broker.

Common QoS Mistakes

  • Assuming QoS 2 end to end while one subscriber uses QoS 0. The weakest link wins.
  • Using QoS 2 for high-rate telemetry. Four packets per message per leg add up quickly, and the in-flight window (Receive Maximum) fills up.
  • Random client IDs with persistent sessions. Offline queuing only works if the client reconnects with the same client ID. A new random ID creates a new, empty session.
  • Ignoring the granted QoS in SUBACK. On a plan capped at QoS 1, a QoS 2 subscription is granted as QoS 1.

Get Started with ConnectMQ

The public test broker is great for learning MQTT QoS, but production traffic belongs on a private broker with credentials. Sign up for free at app.connectmq.com (Serverless plan, no credit card), create a broker, and use the generated username, password and client ID. Upgrade to Serverless (Paid) or Dedicated when you need QoS 2, more connections or larger messages.

Frequently Asked Questions

What is the default MQTT QoS level?

Most client libraries default to QoS 0 for both publish and subscribe. Set the QoS explicitly in your code so the behaviour is clear to anyone reading it.

Does QoS 2 make MQTT slower?

Yes. QoS 2 needs four packets and two round trips per message on each leg, compared with two packets for QoS 1 and one for QoS 0. For most telemetry, QoS 0 or 1 gives better throughput.

Why did my subscriber receive QoS 1 when I published with QoS 2?

The delivered QoS is the minimum of the publish QoS and the granted subscription QoS. Your subscription was granted at QoS 1, either because you requested it or because the broker's Maximum QoS is 1, as on the ConnectMQ Serverless (Free) plan.

Are QoS 0 messages queued for offline clients?

No. Brokers queue only QoS 1 and QoS 2 messages for clients with a persistent session. QoS 0 messages to an offline client are dropped.

Can I use QoS 2 on broker.connectmq.com?

Yes. The anonymous public area supports QoS 0, 1 and 2. Private brokers support QoS 2 on Serverless (Paid) and higher plans, while the free Serverless plan is limited to QoS 1.

Related articles

Why Your MQTT Client Keeps Disconnecting (and How to Fix It)

Reconnect loops are one of the most common MQTT problems. Learn the five usual causes, from duplicate client IDs to keep alive timeouts and oversized packets, and follow a diagnosis flow that pinpoints yours.

8 min read

Who Invented MQTT? The History from Oil Pipelines to IoT

MQTT was invented in 1999 by Andy Stanford-Clark of IBM and Arlen Nipper of Arcom to monitor oil pipelines over satellite. Follow its path to open source, OASIS, ISO and today's managed cloud brokers.

7 min read

MQTT Maximum Message Size: Limits and Large Payload Strategies

MQTT can technically carry a 256 MB packet, but brokers, client libraries and devices impose far lower limits. Learn where the real limits are, what happens when you exceed them, and how to send large payloads.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.