MQTT ne garantit l'ordre des messages que dans un périmètre étroit : les messages d'un publisher, sur un topic, avec un niveau de QoS sont livrés à chaque subscriber dans l'ordre où le broker les a reçus. Entre topics, entre publishers et au sein des abonnements partagés, il n'y a aucune garantie d'ordre — et même dans le périmètre garanti, les retransmissions QoS 1 peuvent produire des doublons tardifs.
Cet article explique précisément ce que promet la spécification, où l'ordre se casse en pratique, et vous donne deux courts scripts Python qui mesurent les messages désordonnés et les doublons sur votre propre broker.
Ce que dit la spécification
MQTT 3.1.1 comme MQTT 5 (section 4.6, « Message ordering ») définissent par défaut chaque topic comme un Ordered Topic. Lorsqu'un broker transmet des messages sur un topic ordonné, il doit envoyer les paquets PUBLISH aux subscribers « pour le même topic et le même QoS » dans l'ordre où il les a reçus de chaque client. Les clients, de leur côté, doivent renvoyer les messages non acquittés et envoyer les acquittements dans l'ordre.
La spécification signale aussi le piège. En QoS 1, un publisher qui envoie 1, 2, 3, 4 peut amener un subscriber à voir 1, 2, 3, 2, 3, 4 : après une reconnexion, les copies retransmises de 2 et 3 arrivent après les originaux. La copie finale de chaque message est dans l'ordre, mais un doublon peut apparaître après son successeur. S'il n'y a jamais plus d'un message en vol à la fois, cela ne peut pas se produire.
| Scénario | Ordre garanti ? |
|---|---|
| Un publisher, un topic, même QoS | Oui, par subscriber |
| Un publisher, plusieurs topics | Non — l'entrelacement entre topics n'est pas défini |
| Plusieurs publishers, un topic | Seulement par publisher ; leur entrelacement est arbitraire |
| Même publisher, QoS mélangés sur un topic | Non — la règle s'applique par QoS |
| QoS 1 après une reconnexion | Copies finales dans l'ordre ; des doublons peuvent arriver en retard |
| QoS 0 sous charge | L'ordre de ce qui arrive est conservé, mais des messages peuvent manquer |
| Groupe d'abonnement partagé | Non — les messages sont répartis entre les membres |
Les niveaux de QoS MQTT détaillent les échanges d'acquittement derrière ces règles.
Où l'ordre se casse en pratique
- Plusieurs messages en vol et reconnexions. Clients et brokers autorisent généralement de nombreux messages QoS 1/2 non acquittés à la fois (en MQTT 5, la limite est négociée via Receive Maximum). Si la connexion tombe avec plusieurs messages en vol, le renvoi peut arriver après des messages plus récents.
- Abonnements partagés.
$share/group/topicrépartit les messages entre consommateurs. Chacun peut voir sa part dans l'ordre, mais le traitement est parallèle, donc le résultat global est désordonné. Voir les abonnements partagés. - Plusieurs publishers. Deux passerelles qui remontent le même appareil produisent deux flux ordonnés indépendamment. Le broker peut les entrelacer n'importe comment.
- Bridges et clusters. Un bridge n'est qu'un client de plus : une connexion de bridge conserve l'ordre, plusieurs chemins entre brokers non. Les brokers en cluster préservent généralement l'ordre par publisher sur un topic, mais consultez la documentation de votre broker.
- Votre propre consommateur. La cause la plus fréquente de toutes : les messages arrivent dans l'ordre, puis sont traités en parallèle (pools de threads, handlers async, files de workers).
Test 1 : mesurer l'ordre sur votre broker
Le publisher numérote les messages par topic et par identifiant de publisher. Le subscriber retient la plus haute séquence vue par (topic, publisher) et signale tout retour en arrière, chaque doublon et tout ce qui manque.
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] # même identifiant de run que le publisher
QOS = int(os.getenv("MQTT_QOS", "1"))
last = defaultdict(lambda: -1) # (topic, publisher) -> séquence la plus haute
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())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}")pip install "paho-mqtt>=2"
python order_sub.py run42 # terminal 1, à lancer en premier
python order_pub.py run42 10000 # terminal 2Sur un broker sain et un réseau stable, la référence doit se terminer avec out_of_order: 0, duplicates: 0 et missing: 0. Avant les scripts, vous pouvez suivre le flux en direct en vous abonnant à testmqtt/order/# dans le client MQTT en ligne pendant que le publisher tourne.
Test 2 : provoquer les cas limites
Changez une seule variable à la fois et comparez avec la référence :
| Expérience | Comment | Ce que vous devriez voir |
|---|---|---|
| Deux publishers | Lancez order_pub.py deux fois en parallèle avec PUB_ID=p1 et PUB_ID=p2 | Le flux de chaque publisher reste ordonné ; ils s'entrelacent |
| QoS 0 sous charge | MQTT_QOS=0, N plus grand, plusieurs publishers | Éventuellement missing > 0, mais aucun désordre |
| Coupure réseau | Bloquez le port 1883 du publisher quelques secondes en cours de run (voir le QoS face à une coupure réseau) | Doublons et compteur dup_flag ; renvois tardifs possibles avec une session persistante |
| Un seul en vol | INFLIGHT=1 plus une coupure réseau | Doublons possibles, mais aucun message après son successeur |
| Abonnement partagé | Abonnez deux copies de order_sub.py à $share/g/testmqtt/order/run42/# | Chaque copie voit des trous — l'autre membre a reçu ces messages |
Pour l'essai avec abonnement partagé, remplacez l'appel subscribe par le filtre $share/... ; vous pouvez vérifier qu'un filtre correspond à vos topics avec le topic matcher. Les doublons ne sont pas un bug du broker — c'est le QoS 1 qui fait son travail ; les messages en double expliquent comment rendre les consommateurs idempotents.
Ne perdez pas l'ordre dans votre propre code
La plupart des signalements « MQTT a mélangé mes messages » viennent du consommateur. En Node.js, un handler de messages async n'attend pas le message précédent :
client.on("message", async (topic, payload) => {
await saveToDb(topic, payload); // le message 2 peut finir avant le message 1
});Chaînez le travail par topic pour conserver l'ordre par topic tout en traitant les différents topics en parallèle :
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);
});Il en va de même pour les pools de threads en Python ou Java et pour les files de workers en aval du broker : partitionnez le travail par topic ou par identifiant d'appareil, jamais en round-robin.
Concevoir en tenant compte de l'ordre
- Placez les événements qui doivent rester ordonnés sur un seul topic issu d'un seul publisher — par exemple
site/device42/events, pas un topic par type d'événement. - Ajoutez un numéro de séquence ou un horodatage source au payload, pour que les consommateurs puissent écarter les mises à jour périmées (« le dernier qui écrit gagne ») ou réordonner de petites fenêtres.
- Rendez les handlers idempotents, car le QoS 1 livrera des doublons.
- Considérez les abonnements partagés comme désordonnés par nature ; partitionnez par appareil si l'ordre compte.
- Utilisez les messages retenus pour l'« état courant », où seule la dernière valeur compte et où l'ordre importe moins.
La garantie d'ordre de MQTT est réelle mais étroite : un publisher, un topic, un QoS. Dix minutes avec un test de numéros de séquence vous montrent comment se comportent votre broker et votre réseau, et les variantes vous montrent exactement où votre conception doit prendre le relais — généralement avec un numéro de séquence dans le payload et un consommateur qui traite chaque topic dans l'ordre.
Questions fréquentes
MQTT garantit-il l'ordre des messages ?
En partie. La spécification impose au broker de livrer les messages d'un même publisher, sur le même topic et avec le même QoS, dans l'ordre où il les a reçus. Il n'y a aucune garantie d'ordre entre topics, entre publishers différents ni entre membres d'un abonnement partagé.
Des messages QoS 1 peuvent-ils arriver dans le désordre ?
Oui, d'une manière précise : après une reconnexion, un message QoS 1 retransmis peut arriver après des messages plus récents, par exemple 1, 2, 3, 2, 3, 4. Les copies finales sont dans l'ordre, mais des doublons peuvent arriver en retard. Garder un seul message en vol à la fois l'évite.
Comment conserver l'ordre des messages MQTT entre plusieurs topics ?
MQTT ne peut pas le faire pour vous. Placez les événements liés sur un seul topic issu d'un seul publisher, ou ajoutez un numéro de séquence ou un horodatage au payload et laissez le consommateur réordonner ou écarter les messages périmés.


