Pruebas de brokers

¿MQTT garantiza el orden de los mensajes? Compruébalo tú mismo

Qué garantiza MQTT sobre el orden de mensajes por topic y QoS, dónde se rompe el orden y scripts en Python que detectan mensajes desordenados y duplicados.

Actualizado · 8 min de lectura

¿MQTT garantiza el orden de los mensajes? Compruébalo tú mismo

MQTT garantiza el orden de los mensajes solo dentro de un ámbito reducido: los mensajes de un publisher, en un topic y con un nivel de QoS se entregan a cada subscriber en el orden en que los recibió el broker. Entre topics, entre publishers y dentro de las suscripciones compartidas no hay garantía de orden, e incluso dentro del ámbito garantizado las retransmisiones de QoS 1 pueden producir duplicados tardíos.

Este artículo explica exactamente qué promete la especificación, dónde se rompe el orden en la práctica, y te da dos scripts cortos en Python que miden mensajes desordenados y duplicados contra tu propio broker.

Qué dice la especificación

Tanto MQTT 3.1.1 como MQTT 5 (sección 4.6, "Message ordering") definen por defecto cada topic como un Ordered Topic. Cuando un broker reenvía mensajes en un topic ordenado, debe enviar los paquetes PUBLISH a los subscribers "para el mismo topic y QoS" en el orden en que los recibió de cada cliente. Los clientes, a su vez, deben reenviar los mensajes no confirmados y enviar las confirmaciones en orden.

La especificación también señala la trampa. Con QoS 1, un publisher que envía 1, 2, 3, 4 puede hacer que un subscriber vea 1, 2, 3, 2, 3, 4: tras una reconexión, las copias retransmitidas de 2 y 3 llegan después de las originales. La copia final de cada mensaje está en orden, pero un duplicado puede aparecer después de su sucesor. Si nunca hay más de un mensaje en vuelo a la vez, esto no puede ocurrir.

Escenario¿Orden garantizado?
Un publisher, un topic, mismo QoSSí, por subscriber
Un publisher, varios topicsNo: el intercalado entre topics no está definido
Varios publishers, un topicSolo por publisher; el intercalado entre ellos es arbitrario
Mismo publisher, QoS mezclados en un topicNo: la regla se aplica por QoS
QoS 1 tras una reconexiónCopias finales en orden; los duplicados pueden llegar tarde
QoS 0 bajo cargaSe mantiene el orden de lo que llega, pero pueden faltar mensajes
Grupo de suscripción compartidaNo: los mensajes se reparten entre los miembros

Niveles de QoS en MQTT explica los flujos de confirmación que hay detrás de estas reglas.

Dónde se rompe el orden en la práctica

  • Varios mensajes en vuelo y reconexiones. Clientes y brokers suelen permitir muchos mensajes QoS 1/2 sin confirmar a la vez (en MQTT 5 el límite se negocia con Receive Maximum). Si la conexión cae con varios en vuelo, el reenvío puede llegar después de mensajes más nuevos.
  • Suscripciones compartidas. $share/group/topic reparte los mensajes entre consumidores. Cada consumidor puede ver su parte en orden, pero el procesamiento corre en paralelo, así que el resultado global está desordenado. Consulta suscripciones compartidas.
  • Varios publishers. Dos gateways que reportan el mismo dispositivo producen dos flujos ordenados de forma independiente. El broker puede intercalarlos de cualquier manera.
  • Bridges y clusters. Un bridge es solo otro cliente, así que una conexión de bridge mantiene el orden; varios caminos entre brokers, no. Los brokers en cluster suelen preservar el orden por publisher en un topic, pero revisa la documentación de tu broker.
  • Tu propio consumidor. La causa más común de todas: los mensajes llegan en orden y luego se procesan de forma concurrente (pools de hilos, handlers async, colas de workers).

Prueba 1: mide el orden en tu broker

El publisher numera los mensajes por topic y por ID de publisher. El subscriber registra la secuencia más alta vista por (topic, publisher) y reporta todo lo que retrocede, cada duplicado y todo lo que falta.

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]                      # mismo id de ejecución que el publisher
QOS = int(os.getenv("MQTT_QOS", "1"))
last = defaultdict(lambda: -1)         # (topic, publisher) -> secuencia más alta
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}")
Ejecutar la línea base
pip install "paho-mqtt>=2"
python order_sub.py run42            # terminal 1, arráncalo primero
python order_pub.py run42 10000      # terminal 2

Con un broker sano y una red estable, la línea base debería terminar con out_of_order: 0, duplicates: 0 y missing: 0. Antes de usar los scripts, puedes ver el flujo en directo suscribiéndote a testmqtt/order/# en el cliente MQTT online mientras corre el publisher.

Prueba 2: provoca los casos límite

Cambia una variable cada vez y compara con la línea base:

ExperimentoCómoQué deberías ver
Dos publishersEjecuta order_pub.py dos veces en paralelo con PUB_ID=p1 y PUB_ID=p2El flujo de cada publisher sigue ordenado; se intercalan
QoS 0 bajo cargaMQTT_QOS=0, un N mayor, varios publishersPosiblemente missing > 0, pero sin desorden
Corte de redBloquea el puerto 1883 del publisher unos segundos a mitad de la ejecución (ver QoS con un corte de red)Duplicados y conteos de dup_flag; reenvíos tardíos posibles con sesión persistente
Uno en vueloINFLIGHT=1 más un corte de redPosibles duplicados, pero ningún mensaje después de su sucesor
Suscripción compartidaSuscribe dos copias de order_sub.py a $share/g/testmqtt/order/run42/#Cada copia ve huecos: el otro miembro recibió esos mensajes

Para la prueba con suscripción compartida, cambia la llamada de suscripción al filtro $share/...; puedes comprobar que un filtro coincide con tus topics con el topic matcher. Los duplicados no son un bug del broker: es QoS 1 haciendo su trabajo. Mensajes duplicados explica cómo hacer que los consumidores sean idempotentes.

No pierdas el orden en tu propio código

La mayoría de los informes de "MQTT desordenó mis mensajes" vienen del consumidor. En Node.js, un handler de mensajes async no espera al mensaje anterior:

Desordenado: los handlers se solapan
client.on("message", async (topic, payload) => {
  await saveToDb(topic, payload); // el mensaje 2 puede terminar antes que el 1
});

Encadena el trabajo por topic para mantener el orden por topic sin dejar de procesar topics distintos en paralelo:

Ordenado por 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);
});

Lo mismo se aplica a los pools de hilos en Python o Java y a las colas de workers detrás del broker: reparte el trabajo por topic o ID de dispositivo, nunca en round-robin.

Diseñar pensando en el orden

  • Pon los eventos que deben mantenerse ordenados en un solo topic desde un solo publisher, por ejemplo site/device42/events, no un topic por tipo de evento.
  • Añade un número de secuencia o un timestamp de origen al payload, para que los consumidores puedan descartar actualizaciones obsoletas ("gana el último que escribe") o reordenar ventanas pequeñas.
  • Haz que los handlers sean idempotentes, porque QoS 1 entregará duplicados.
  • Trata las suscripciones compartidas como desordenadas por diseño; reparte por dispositivo si el orden importa.
  • Usa mensajes retenidos para el "estado actual", donde solo cuenta el último valor y el orden importa menos.

La garantía de orden de MQTT es real pero estrecha: un publisher, un topic, un QoS. Diez minutos con una prueba de números de secuencia te muestran cómo se comportan tu broker y tu red, y las variantes te muestran exactamente dónde debe tomar el relevo tu diseño, normalmente con un número de secuencia en el payload y un consumidor que procesa cada topic en orden.

Preguntas frecuentes

¿MQTT garantiza el orden de los mensajes?

En parte. La especificación exige que el broker entregue los mensajes de un mismo publisher, en el mismo topic y con el mismo QoS, en el orden en que los recibió. No hay garantía de orden entre topics, entre publishers distintos ni entre miembros de una suscripción compartida.

¿Pueden llegar desordenados los mensajes QoS 1?

Sí, de una forma concreta: tras una reconexión, un mensaje QoS 1 retransmitido puede llegar después de mensajes posteriores, por ejemplo 1, 2, 3, 2, 3, 4. Las copias finales están en orden, pero pueden aparecer duplicados tarde. Mantener un solo mensaje en vuelo lo evita.

¿Cómo mantengo el orden de los mensajes MQTT entre varios topics?

MQTT no puede hacerlo por ti. Pon los eventos relacionados en un solo topic desde un solo publisher, o añade un número de secuencia o un timestamp al payload y deja que el consumidor reordene o descarte los mensajes obsoletos.