Pruebas de brokers

Cómo probar la lógica de reconexión y el backoff en MQTT

Prueba la reconexión MQTT: simula reinicios del broker y cortes de red silenciosos, verifica el backoff exponencial, la resuscripción y la sesión persistente.

Actualizado · 9 min de lectura

Cómo probar la lógica de reconexión y el backoff en MQTT

El código de reconexión es la parte de un cliente MQTT que menos se ejecuta en desarrollo y más en producción. Para probar la lógica de reconexión MQTT, rompe la conexión a propósito de las mismas formas en que lo hace el mundo real (reinicio del broker, conexiones rechazadas, un corte de red silencioso) y comprueba cuatro cosas: el cliente lo detecta, espera con retardos crecientes, se vuelve a suscribir y no pierde mensajes QoS 1 mientras estuvo fuera.

Esta guía te da un cliente de prueba en Python que lo registra todo, tres escenarios de fallo que puedes reproducir en minutos y una checklist de verificaciones.

Cómo es un comportamiento de reconexión "correcto"

ComportamientoQué verificar
DetecciónLas conexiones rechazadas o reseteadas se detectan al instante; los cortes silenciosos en ~1–2 intervalos de keep-alive
BackoffLos retardos crecen (por ejemplo 1, 2, 4, 8 … s) hasta un tope, y se reinician tras una conexión correcta
JitterLos dispositivos no reintentan al unísono; si no, provocas una tormenta de reconexiones
ResuscripciónLas suscripciones se restauran, tanto si el broker conservó la sesión como si no
Reanudación de sesiónCon una sesión persistente, los mensajes QoS 1/2 encolados llegan tras reconectar
Rendirse bienLos fallos de autenticación se registran claramente en lugar de martillear al broker para siempre

La parte de diseño (cómo elegir los retardos y la configuración de sesión) se trata en buenas prácticas de reconexión MQTT. Aquí nos centramos en demostrar que tu cliente lo hace.

Cómo se reconectan las librerías habituales

  • paho-mqtt (Python 2.x) se reconecta automáticamente dentro de loop_forever() y loop_start(). reconnect_delay_set(min_delay, max_delay) controla el backoff exponencial; no hay jitter.
  • MQTT.js 5 se reconecta automáticamente cada reconnectPeriod milisegundos (1000 por defecto): un intervalo fijo, no exponencial. Por defecto vuelve a suscribirse a los topics anteriores (resubscribe: true).
  • Eclipse Paho Java se reconecta si activas setAutomaticReconnect(true), duplicando la espera hasta un máximo configurable.
  • MQTTnet deja la reconexión en manos de tu código: gestionas el evento de desconexión y vuelves a llamar a connect con tu propio retardo.

Sea cual sea la librería, los valores por defecto rara vez son lo que quieres en producción, y precisamente por eso merecen una prueba.

Un cliente de prueba que registra cada transición

Este cliente con paho-mqtt 2.x usa MQTT 5 con una sesión persistente, se resuscribe en cada conexión e imprime una marca de tiempo monotónica para cada evento, de modo que puedes leer el backoff directamente en la salida:

reconnect_probe.py
import os, time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

HOST = os.getenv("MQTT_HOST", "localhost")
PORT = int(os.getenv("MQTT_PORT", "1883"))
TOPIC = "testmqtt/reconnect/probe/#"
T0 = time.monotonic()

def log(event, **info):
    print(f"{time.monotonic() - T0:8.2f}s  {event:<15} {info}", flush=True)

def on_connect(client, userdata, flags, reason_code, properties):
    log("connected", rc=str(reason_code), session_present=flags.session_present)
    if not reason_code.is_failure:
        client.subscribe(TOPIC, qos=1)   # resuscribirse siempre

def on_disconnect(client, userdata, flags, reason_code, properties):
    log("disconnected", rc=str(reason_code))

def on_connect_fail(client, userdata):
    log("connect_failed")

def on_message(client, userdata, msg):
    log("message", topic=msg.topic, payload=msg.payload.decode(), dup=msg.dup)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id="reconnect-probe-1", protocol=mqtt.MQTTv5)
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect, client.on_disconnect = on_connect, on_disconnect
client.on_connect_fail, client.on_message = on_connect_fail, on_message
client.reconnect_delay_set(min_delay=1, max_delay=32)

props = Properties(PacketTypes.CONNECT)
props.SessionExpiryInterval = 3600       # conservar la sesión 1 hora
client.connect_async(HOST, PORT, keepalive=10, clean_start=False, properties=props)
client.loop_forever(retry_first_connection=True)

Mantén fijo el client ID durante las pruebas: una sesión persistente pertenece a un client ID, y dos procesos con el mismo ID se expulsarán mutuamente una y otra vez. Un keepalive=10 corto hace que los cortes silenciosos aparezcan rápido; usa tu valor de producción en la ejecución final.

Escenario 1: reinicio y caída del broker

Levanta un Mosquitto desechable en local para poder detenerlo cuando quieras:

Broker local que puedes tumbar
docker run -d --name mqtt-reconnect -p 1883:1883 eclipse-mosquitto:2 \
  mosquitto -c /mosquitto-no-auth.conf
python reconnect_probe.py            # terminal 1

docker stop mqtt-reconnect && sleep 60 && docker start mqtt-reconnect   # terminal 2

Salida esperada, con retardos que se duplican hasta que vuelve el broker:

Así se ve un backoff correcto
    0.03s  connected       {'rc': 'Success', 'session_present': False}
   12.40s  disconnected    {'rc': 'Unspecified error'}
   13.41s  connect_failed  {}
   15.42s  connect_failed  {}
   19.43s  connect_failed  {}
   27.44s  connect_failed  {}
   43.45s  connect_failed  {}
   75.46s  connected       {'rc': 'Success', 'session_present': False}

Los textos exactos de los reason codes varían según la versión, pero los intervalos entre intentos deberían ser de 1, 2, 4, 8, 16 y 32 segundos. Fíjate en session_present: False tras el reinicio: este Mosquitto no tiene persistencia, así que la sesión murió con el proceso. Tu cliente debe volver a suscribirse; si dejan de llegar mensajes tras esta prueba, ese es el bug. Activa persistence true en Mosquitto (ver persistencia en Mosquitto) y repite para ver cómo la sesión sobrevive a un reinicio.

Escenario 2: corte de red silencioso

Un reinicio del broker cierra los sockets limpiamente. Un punto de acceso Wi-Fi caído o un timeout de NAT no: los paquetes simplemente desaparecen y el cliente solo se entera por el keep-alive. En una máquina Linux puedes simularlo con una regla de firewall:

Descartar todo el tráfico al puerto 1883 y luego restaurarlo
sudo iptables -I OUTPUT -p tcp --dport 1883 -j DROP
sleep 45
sudo iptables -D OUTPUT -p tcp --dport 1883 -j DROP

Usa -j REJECT --reject-with tcp-reset en lugar de -j DROP para simular un reset inmediato. La diferencia importa: con DROP, la sonda debería registrar disconnected unos 10–20 segundos después de añadir la regla (uno o dos intervalos de keep-alive) y luego aplicar backoff hasta que se elimine la regla. Si nunca registra una desconexión, el keep-alive no funciona: consulta keep alive en MQTT.

Para latencia, pérdida de paquetes, límites de ancho de banda o entornos multiplataforma, un proxy es más flexible que iptables; probar con Toxiproxy muestra cómo inyectar esos fallos entre cliente y broker.

Escenario 3: reanudar la sesión con mensajes encolados

Esta es la prueba que la mayoría de los equipos se salta y de la que salen la mayoría de los incidentes. Apunta la sonda a un broker que conserve sesiones, por ejemplo el endpoint compartido broker.connectmq.com, y:

  1. Arranca reconnect_probe.py y espera a connected con la suscripción activa.
  2. Bloquea el puerto 1883 con la regla de iptables del escenario 2.
  3. Mientras la sonda está desconectada, publica cinco mensajes QoS 1 en testmqtt/reconnect/probe/q desde un camino que la regla no bloquee, por ejemplo el cliente MQTT online, que se conecta por WebSockets seguros en el puerto 8084.
  4. Elimina la regla y observa cómo la sonda se reconecta.

La sonda debería registrar session_present: True y después los cinco mensajes. Si se reconecta con session_present: False, el broker descartó la sesión: revisa clean_start, el intervalo de expiración de la sesión y si tu client ID cambió. Si los mensajes llegan con dup: True o dos veces, es el comportamiento at-least-once de QoS 1 funcionando según lo previsto: tu handler debe ser idempotente. Sesiones persistentes explica qué guarda el broker y durante cuánto tiempo.

Más casos de fallo que merecen una prueba

  • Credenciales cambiadas mientras estaba desconectado. El broker responde a la reconexión con "Not authorized" o "Bad user name or password". El cliente debería registrarlo de forma visible y frenar, no reintentar cada segundo para siempre. Busca los códigos con la herramienta de reason codes.
  • Fallo de DNS. Desconecta la red por completo; los errores de resolución deben pasar por el mismo camino de backoff que las conexiones rechazadas.
  • Client ID duplicado. Arranca la sonda dos veces. Cada instancia expulsa a la otra y ambas se reconectan sin fin: un bucle clásico de producción.
  • Caídas largas. Deja el broker apagado más tiempo que max_delay y comprueba que los intentos se estabilizan en el tope en lugar de seguir creciendo o detenerse.
  • Reconexión durante una publicación. Publica mensajes QoS 1 en bucle mientras cortas la red y cuenta lo que llega a un subscriber tras la recuperación.

Checklist de pruebas de reconexión

  1. Registra cada conexión, desconexión e intento fallido con una marca de tiempo monotónica.
  2. Reinicia el broker: verifica la detección, los retardos crecientes y la recuperación.
  3. Descarta tráfico en silencio: verifica la detección por keep-alive en uno o dos intervalos.
  4. Encola mensajes QoS 1 mientras estás desconectado: verifica session_present y la entrega tras reconectar.
  5. Verifica la resuscripción tras un reinicio sin persistencia.
  6. Prueba credenciales incorrectas, fallos de DNS y client IDs duplicados.
  7. Automatiza los escenarios en CI con un contenedor de broker desechable.

La lógica de reconexión solo es tan buena como la última vez que la viste funcionar. Una sonda que imprime marcas de tiempo, un broker que puedes detener y una regla de firewall bastan para demostrar el backoff, la resuscripción y la reanudación de sesión, y para detectar la regresión antes de que lo haga un reinicio del broker.

Preguntas frecuentes

¿Por qué mi cliente MQTT no recibe mensajes después de reconectarse?

Normalmente la sesión no se reanudó: el cliente se conectó con una sesión limpia (o el broker perdió la sesión al reiniciarse) y no volvió a suscribirse. Resuscríbete siempre en el callback de conexión y revisa el flag session present del CONNACK.

¿Cuánto tarda un cliente MQTT en detectar una conexión muerta?

Si la red cae en silencio, el cliente solo se entera cuando su ping de keep-alive queda sin respuesta, normalmente en uno o dos intervalos de keep-alive. Una conexión rechazada o reseteada se detecta de inmediato.

¿paho-mqtt usa backoff exponencial?

Sí. Con loop_forever() o loop_start(), paho-mqtt se reconecta automáticamente y duplica la espera desde min_delay hasta max_delay, configurados con reconnect_delay_set(). No añade jitter aleatorio, así que añade el tuyo si muchos dispositivos comparten el mismo calendario.