Broker Testing

Does MQTT Guarantee Message Order? Test It Yourself

What MQTT really guarantees about message order per topic and QoS, where ordering breaks, and Python scripts that detect out-of-order and duplicate messages.

Updated · 8 min read

Does MQTT Guarantee Message Order? Test It Yourself

MQTT guarantees message order only within a narrow scope: messages from one publisher, on one topic, at one QoS level are delivered to each subscriber in the order the broker received them. Across topics, across publishers and inside shared subscriptions there is no ordering guarantee — and even within the guaranteed scope, QoS 1 retransmissions can produce late duplicates.

This article explains exactly what the specification promises, where ordering breaks in practice, and gives you two short Python scripts that measure out-of-order and duplicate messages against your own broker.

What the specification says

Both MQTT 3.1.1 and MQTT 5 (section 4.6, "Message ordering") define every topic as an Ordered Topic by default. When a broker forwards messages on an ordered topic, it must send PUBLISH packets to subscribers "for the same Topic and QoS" in the order they were received from any given client. Clients, in turn, must resend unacknowledged messages and send acknowledgements in order.

The specification also points out the catch. With QoS 1, a publisher sending 1, 2, 3, 4 can lead a subscriber to see 1, 2, 3, 2, 3, 4: after a reconnect, the retransmitted copies of 2 and 3 arrive after the originals. The final copy of each message is in order, but a duplicate can show up after its successor. If no more than one message is in flight at a time, this cannot happen.

ScenarioOrder guaranteed?
One publisher, one topic, same QoSYes, per subscriber
One publisher, several topicsNo — interleaving between topics is undefined
Several publishers, one topicOnly per publisher; interleaving between them is arbitrary
Same publisher, mixed QoS on one topicNo — the rule applies per QoS
QoS 1 after a reconnectFinal copies in order; duplicates can arrive late
QoS 0 under loadOrder kept for what arrives, but messages can be missing
Shared subscription groupNo — messages are spread across members

MQTT QoS levels covers the acknowledgement flows behind these rules.

Where ordering breaks in practice

  • Multiple in-flight messages and reconnects. Clients and brokers usually allow many unacknowledged QoS 1/2 messages at once (in MQTT 5 the limit is negotiated with Receive Maximum). If the connection drops with several in flight, the resend can land after newer messages.
  • Shared subscriptions. $share/group/topic distributes messages across consumers. Each consumer may see its share in order, but processing runs in parallel, so the overall result is unordered. See shared subscriptions.
  • Several publishers. Two gateways reporting the same device produce two independently ordered streams. The broker may interleave them in any way.
  • Bridges and clusters. A bridge is just another client, so one bridge connection keeps order; multiple paths between brokers do not. Clustered brokers generally preserve per-publisher order on a topic, but check your broker's documentation.
  • Your own consumer. The most common cause of all — messages arrive in order and are then processed concurrently (thread pools, async handlers, worker queues).

Test 1: measure ordering on your broker

The publisher numbers messages per topic and per publisher ID. The subscriber tracks the highest sequence it has seen per (topic, publisher) and reports anything that goes backwards, any duplicate and anything missing.

order_sub.py
import os, sys, time
from collections import Counter, defaultdict
import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
RUN = sys.argv[1]                      # same run id as the publisher
QOS = int(os.getenv("MQTT_QOS", "1"))
last = defaultdict(lambda: -1)         # (topic, publisher) -> highest seq
seen = defaultdict(set)
stats = Counter()

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"testmqtt/order/{RUN}/#", qos=QOS)

def on_message(client, userdata, msg):
    pub_id, seq = msg.payload.decode().split(":")
    key, seq = (msg.topic, pub_id), int(seq)
    stats["received"] += 1
    stats["dup_flag"] += msg.dup
    if seq in seen[key]:
        stats["duplicates"] += 1
        return
    seen[key].add(seq)
    if seq < last[key]:
        stats["out_of_order"] += 1
        print(f"OUT OF ORDER {key}: {seq} after {last[key]}")
    last[key] = max(last[key], seq)

def missing():
    return sum(last[k] + 1 - len(seen[k]) for k in seen)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect, client.on_message = on_connect, on_message
client.connect(HOST, 1883)
client.loop_start()
try:
    while True:
        time.sleep(5)
        print(dict(stats), "missing:", missing())
except KeyboardInterrupt:
    print("FINAL", dict(stats), "missing:", missing())
order_pub.py
import os, sys
import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
RUN, N = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 10000
PUB_ID = os.getenv("PUB_ID", "p1")
TOPICS = int(os.getenv("TOPICS", "4"))
QOS = int(os.getenv("MQTT_QOS", "1"))

pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
if os.getenv("MQTT_USER"):
    pub.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
pub.max_inflight_messages_set(int(os.getenv("INFLIGHT", "20")))
pub.connect(HOST, 1883)
pub.loop_start()

seq, infos = [0] * TOPICS, []
for i in range(N):
    t = i % TOPICS
    infos.append(pub.publish(f"testmqtt/order/{RUN}/t{t}", f"{PUB_ID}:{seq[t]}", qos=QOS))
    seq[t] += 1
for info in infos:
    info.wait_for_publish(timeout=60)
pub.disconnect()
pub.loop_stop()
print(f"published {N} messages on {TOPICS} topics as {PUB_ID}")
Run the baseline
pip install "paho-mqtt>=2"
python order_sub.py run42            # terminal 1, start first
python order_pub.py run42 10000      # terminal 2

On a healthy broker and a stable network, the baseline should end with out_of_order: 0, duplicates: 0 and missing: 0. Before scripting, you can watch the stream live by subscribing to testmqtt/order/# in the online MQTT client while the publisher runs.

Test 2: provoke the edge cases

Change one variable at a time and compare with the baseline:

ExperimentHowWhat you should see
Two publishersRun order_pub.py twice in parallel with PUB_ID=p1 and PUB_ID=p2Each publisher's stream stays ordered; they interleave
QoS 0 under loadMQTT_QOS=0, larger N, several publishersPossibly missing > 0, still no out-of-order
Network cutBlock the publisher's port 1883 for a few seconds mid-run (see QoS with a network cut)Duplicates and dup_flag counts; late re-sends possible with a persistent session
One in flightINFLIGHT=1 plus a network cutDuplicates possible, but no message after its successor
Shared subscriptionSubscribe two order_sub.py copies to $share/g/testmqtt/order/run42/#Each copy sees gaps — the other member got those messages

For the shared-subscription run, change the subscribe call to the $share/... filter; you can check that a filter matches your topics with the topic matcher. Duplicates are not a broker bug — they are QoS 1 doing its job; duplicate messages covers how to make consumers idempotent.

Don't lose order in your own code

Most "MQTT reordered my messages" reports come from the consumer. In Node.js, an async message handler does not wait for the previous message:

Unordered: handlers overlap
client.on("message", async (topic, payload) => {
  await saveToDb(topic, payload); // message 2 can finish before message 1
});

Chain the work per topic to keep per-topic order while still processing different topics in parallel:

Ordered per topic
const chains = new Map();
client.on("message", (topic, payload) => {
  const prev = chains.get(topic) ?? Promise.resolve();
  const next = prev.then(() => saveToDb(topic, payload)).catch(console.error);
  chains.set(topic, next);
});

The same applies to thread pools in Python or Java and to worker queues downstream of the broker: partition work by topic or device ID, never round-robin.

Designing for order

  • Put events that must stay ordered on one topic from one publisher — for example site/device42/events, not one topic per event type.
  • Add a sequence number or source timestamp to the payload, so consumers can drop stale updates ("last writer wins") or reorder small windows.
  • Make handlers idempotent, because QoS 1 will deliver duplicates.
  • Treat shared subscriptions as unordered by design; partition by device if order matters.
  • Use retained messages for "current state", where only the latest value counts and order matters less.

MQTT's ordering guarantee is real but narrow: one publisher, one topic, one QoS. Ten minutes with a sequence-number test shows you how your broker and network behave, and the variants show you exactly where your design must take over — usually with a sequence number in the payload and a consumer that processes each topic in order.

Frequently asked questions

Does MQTT guarantee message order?

Partly. The specification requires a broker to deliver messages from one publisher on the same topic and QoS in the order it received them. There is no ordering guarantee across topics, across different publishers, or between members of a shared subscription.

Can QoS 1 messages arrive out of order?

Yes, in one specific way: after a reconnect, a retransmitted QoS 1 message can arrive after later messages, for example 1, 2, 3, 2, 3, 4. The final copies are in order, but duplicates may appear late. Keeping one message in flight at a time avoids it.

How do I keep MQTT messages in order across topics?

MQTT cannot do it for you. Put related events on one topic from one publisher, or add a sequence number or timestamp to the payload and let the consumer reorder or discard stale messages.