MQTT gestionado en la nube

Diseñar clientes IoT que sobrevivan a la caída de un broker MQTT

Todos los brokers se reinician tarde o temprano. Aprende los patrones del lado del cliente que convierten la caída de un broker MQTT en algo sin consecuencias: reconexiones con jitter, búfer sin conexión, estado con Last Will, gestión de sesiones y comandos idempotentes.

Dispositivos IoT almacenando datos localmente durante la caída de un broker MQTT y reconectándose con backoff cuando el broker vuelve
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

La caída de un broker MQTT no es una cuestión de si ocurrirá, sino de cuándo: las redes se particionan, los certificados rotan, los brokers se actualizan y las regiones de nube tienen días malos. Esta guía te muestra cómo diseñar clientes de dispositivo y de backend para que una caída del broker, corta o larga, no pierda datos importantes ni provoque acciones duplicadas. Los patrones se aplican a cualquier broker, incluido ConnectMQ, y vienen con código Python funcional.

Qué ocurre realmente durante la caída de un broker MQTT

Desde el punto de vista del cliente, una caída se presenta de una de tres formas: la conexión TCP se corta, la conexión se queda colgada en silencio hasta que expira el keepalive o se rechazan los nuevos intentos de conexión. La causa puede ser el propio broker, la red entre tú y el broker, el DNS, TLS o incluso tus propias credenciales. El cliente no siempre puede distinguirlo, así que debe tratarlos todos igual: detectarlo rápido, seguir trabajando en local y reconectarse con educación.

Hay cuatro cosas en riesgo:

  1. Los datos salientes publicados durante la desconexión se pierden si no los almacenas en un búfer.
  2. Los comandos entrantes enviados mientras estabas desconectado pueden entregarse o no, según la configuración de la sesión.
  3. La información de presencia se vuelve errónea si otros sistemas siguen creyendo que el dispositivo está en línea.
  4. El propio broker puede verse desbordado cuando miles de dispositivos se reconectan en el mismo instante.

Un buen diseño de cliente aborda cada una. Merece la pena leer qué hace un broker MQTT con las sesiones y los mensajes en cola si esos términos no te resultan familiares.

Patrón 1: reconectar con backoff exponencial y jitter

El enfoque ingenuo es reintentar cada segundo indefinidamente. Multiplica eso por diez mil dispositivos y, en cuanto el broker vuelve, recibe un muro de conexiones simultáneas que puede tumbarlo de nuevo de inmediato. Es el problema de la estampida (thundering herd).

La solución es el backoff exponencial con jitter aleatorio: espera 1 s, luego 2 s, 4 s, 8 s y así hasta un tope, y aleatoriza cada retraso para que los dispositivos se repartan. Paho 2.x lo admite de serie mediante reconnect_delay_set, pero no añade jitter, así que muchos equipos gestionan el bucle por su cuenta para tener control total.

Ten también cuidado con las credenciales. Los brokers que bloquean direcciones IP tras inicios de sesión fallidos repetidos, como hace ConnectMQ, tratarán como un ataque a un dispositivo con una contraseña incorrecta que reintenta cada segundo. Aplica un backoff mucho más agresivo ante errores de autenticación que ante errores de red, y avisa de ellos a un operador.

Patrón 2: almacenar localmente los datos salientes

Las bibliotecas cliente de MQTT guardan en memoria los mensajes en vuelo, que desaparecen si el dispositivo se reinicia durante la caída. Para la telemetría que no te puedes permitir perder, escribe primero cada lectura en un pequeño almacén local y bórrala solo cuando el broker la confirme con un PUBACK. SQLite encaja bien en gateways y dispositivos Linux; en microcontroladores, un búfer circular en flash cumple la misma función.

Decide de antemano qué pasa cuando el búfer se llena. Para la mayoría de la telemetría, descartar las lecturas más antiguas es lo correcto. Para eventos de facturación o de seguridad, quizá prefieras dejar de muestrear los datos no críticos.

Patrón 3: anunciar la presencia con Last Will y estado retenido

Define un Last Will and Testament (LWT) al conectarte: un mensaje offline retenido que el broker publica en tu nombre si desapareces sin una desconexión limpia. En cada conexión correcta, publica un online retenido en el mismo topic. Así, los demás sistemas ven una presencia precisa sin necesidad de sondear.

Sé preciso con la semántica del LWT. El will se publica cuando el broker detecta que la sesión terminó de forma anómala, normalmente tras 1,5 veces el intervalo de keepalive. Si el propio broker está caído, no se publica nada hasta que vuelve, y tras un reinicio del broker puede que los clientes tengan que restablecer el estado retenido. MQTT 5 añade un intervalo de retraso del will, que permite suprimir los mensajes offline intermitentes durante una reconexión breve.

Patrón 4: hacer que los comandos sean idempotentes

Con QoS 1, un mensaje se entrega al menos una vez. Durante una caída y la posterior reconexión, los duplicados se vuelven probables. Un comando "conmutar relé" entregado dos veces vuelve a apagar el relé; "dispensar 10 ml" entregado dos veces dispensa 20 ml. El remedio es hacer que todos los comandos sean idempotentes:

  • Envía estados absolutos ({"relay": "on"}) en lugar de acciones relativas (toggle).
  • Incluye un command_id único en cada comando y haz que el dispositivo recuerde los IDs ejecutados recientemente.
  • Incluye una caducidad: un intervalo de expiración de mensaje de MQTT 5 o una marca de tiempo valid_until, para que un comando en cola durante una caída larga no se ejecute horas después, cuando ya no sea seguro.

Un cliente Python resiliente

El siguiente cliente combina los cuatro patrones. Almacena la telemetría en SQLite, se reconecta con backoff exponencial aleatorizado, mantiene un topic de estado retenido con LWT y elimina los comandos duplicados. Usa el servidor MQTT público gratuito para que puedas ejecutarlo de inmediato (solo con datos de prueba, nunca de producción); para producción, cambia al puerto 8883, TLS y las credenciales de tu portal.

import json
import random
import sqlite3
import time
import uuid

import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 1883
DEVICE = f"dev-{uuid.uuid4().hex[:6]}"
ROOT = f"connectmq/demo/{DEVICE}"
STATUS, TELEMETRY, COMMANDS = f"{ROOT}/status", f"{ROOT}/telemetry", f"{ROOT}/cmd"

db = sqlite3.connect("outbox.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS outbox (id INTEGER PRIMARY KEY, payload TEXT)")
db.commit()

pending = {}            # mid -> id de fila del outbox
seen_commands = []      # IDs de comandos recientes para eliminar duplicados
connected = False


def on_connect(client, userdata, flags, reason_code, properties):
    global connected
    if reason_code.is_failure:
        print("Connect refused:", reason_code)
        return
    connected = True
    print("Connected")
    client.publish(STATUS, "online", qos=1, retain=True)
    client.subscribe(COMMANDS, qos=1)


def on_disconnect(client, userdata, flags, reason_code, properties):
    global connected
    connected = False
    pending.clear()  # las filas sin confirmar siguen en el outbox y se reenvían
    print("Disconnected:", reason_code)


def on_publish(client, userdata, mid, reason_code, properties):
    row_id = pending.pop(mid, None)
    if row_id is not None:
        db.execute("DELETE FROM outbox WHERE id = ?", (row_id,))
        db.commit()


def on_message(client, userdata, msg):
    try:
        cmd = json.loads(msg.payload)
    except ValueError:
        print("Ignoring malformed command")
        return
    if cmd.get("command_id") in seen_commands:
        print("Duplicate command ignored:", cmd["command_id"])
        return
    if cmd.get("valid_until", float("inf")) < time.time():
        print("Expired command ignored:", cmd.get("command_id"))
        return
    seen_commands.append(cmd.get("command_id"))
    del seen_commands[:-100]
    print("Executing:", cmd)


def flush_outbox(client):
    rows = db.execute("SELECT id, payload FROM outbox ORDER BY id LIMIT 20").fetchall()
    for row_id, payload in rows:
        if row_id in pending.values():
            continue
        info = client.publish(TELEMETRY, payload, qos=1)
        pending[info.mid] = row_id


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=DEVICE)
client.will_set(STATUS, "offline", qos=1, retain=True)
client.on_connect, client.on_disconnect = on_connect, on_disconnect
client.on_publish, client.on_message = on_publish, on_message

# Backoff exponencial entre reconexiones; un mínimo aleatorio reparte los dispositivos
client.reconnect_delay_set(min_delay=random.randint(1, 5), max_delay=120)
client.connect_async(HOST, PORT, keepalive=30)  # el bucle lo reintenta si falla
client.loop_start()

while True:
    reading = {"temperature": round(random.uniform(20, 25), 2), "ts": int(time.time())}
    db.execute("INSERT INTO outbox (payload) VALUES (?)", (json.dumps(reading),))
    db.commit()
    if connected:
        flush_outbox(client)
    time.sleep(5)

Pruébalo desconectando la red durante un minuto. Las lecturas se acumulan en outbox.db, el bucle de red sigue reintentando con retrasos crecientes y, cuando vuelve la red, la cola pendiente se vacía en orden, hasta veinte filas por ciclo. Paho duplica el retraso entre intentos hasta max_delay, pero no añade jitter por sí mismo; por eso cada dispositivo elige un retraso inicial aleatorio. En una flota grande puedes ir más allá y añadir jitter aleatorio a cada intento con tu propio bucle de reconexión.

El estado de sesión y lo que no debes dar por hecho

Las sesiones persistentes (clean session a false en MQTT 3.1.1, o un intervalo de expiración de sesión en MQTT 5) permiten al broker poner en cola los mensajes QoS 1 y 2 para un cliente desconectado. Ayudan durante las caídas del lado del cliente. Durante las caídas del lado del broker solo ayudan si el broker conserva las sesiones entre reinicios, algo que no todos los brokers hacen. ConnectMQ no garantiza la persistencia entre reinicios, y en el broker público las sesiones expiran como máximo a los 30 minutos.

Así que diseña de forma defensiva:

  1. Vuelve a suscribirte en on_connect cada vez, en lugar de confiar en que el broker recuerde las suscripciones.
  2. Vuelve a publicar el estado y la configuración retenidos tras reconectar.
  3. Trata el búfer del dispositivo, no las colas del broker, como la fuente de verdad para los datos que no puedes perder.
  4. Deja que el backend reconcilie el estado tras un hueco, por ejemplo pidiendo a los dispositivos que vuelvan a publicar su configuración actual.

Checklist de preparación ante caídas

Área Pregunta Buena respuesta
Reconexión ¿Usan todos los clientes backoff con jitter? Sí, con un tope de uno a dos minutos
Errores de autenticación ¿Se reintentan despacio las credenciales erróneas y se alerta? Sí, por separado de los errores de red
Búfer ¿Dónde viven los datos no enviados durante una caída? En un almacén local persistente con límite de tamaño
Presencia ¿Cómo se enteran los demás de que un dispositivo ha desaparecido? LWT más estado retenido
Comandos ¿Qué pasa si un comando llega dos veces? Nada; los comandos son idempotentes
Comandos obsoletos ¿Puede ejecutarse un comando antiguo tras una caída larga? No; se aplica la caducidad
Suscripciones ¿Se restauran las suscripciones tras reconectar? Sí, en on_connect
Pruebas ¿Has ensayado una caída? Sí, cortando la red en una prueba

Para las medidas del lado del broker que complementan estos patrones de cliente, consulta nuestras guías sobre alta disponibilidad de un broker MQTT y monitorización de un broker MQTT, y repasa el checklist de MQTT en producción antes del lanzamiento.

Ejecuta clientes resilientes en ConnectMQ

Un broker MQTT gestionado elimina las caídas autoinfligidas más habituales, como los certificados caducados y los servidores sin parchear, mientras tus clientes se encargan del resto. Crea un broker gratuito en app.connectmq.com con el plan Serverless, sin tarjeta de crédito, prueba estos patrones contra él y usa el estado de conexión $SYS y el registro de alertas del portal para vigilar las reconexiones. Descubre más sobre ConnectMQ o compara opciones en nuestra guía de broker MQTT en la nube.

Preguntas frecuentes

¿Con qué rapidez debe reconectarse un cliente IoT tras la caída de un broker MQTT?

Intenta rápido al principio y después aplica un backoff exponencial hasta un tope de uno a dos minutos, con jitter aleatorio en cada retraso. Así se recupera rápido de los cortes breves sin desbordar el broker tras una caída larga.

¿Protege QoS 1 los datos durante la caída de un broker?

Solo los mensajes ya entregados a la biblioteca cliente, y solo mientras el dispositivo siga alimentado. Para los datos que no puedes perder, guárdalos en un almacenamiento local persistente hasta que llegue un PUBACK.

¿Cuándo se envía el mensaje Last Will?

Cuando el broker detecta una desconexión anómala, normalmente tras aproximadamente 1,5 veces el intervalo de keepalive. No se envía en una desconexión limpia, y no se envía nada mientras el propio broker esté caído.

¿Deben usar QoS 2 los comandos para evitar duplicados?

QoS 2 evita los duplicados entre el cliente y el broker, pero pueden seguir produciéndose duplicados de extremo a extremo tras las reconexiones o los reintentos de la aplicación. Los comandos idempotentes con IDs únicos son la solución más robusta.

Artículos relacionados

Broker MQTT BYOC: el modelo Bring Your Own Cloud explicado

Un broker MQTT BYOC se ejecuta en tu cuenta cloud, pero lo opera un proveedor. Descubre cómo funciona el modelo, cómo se reparten las responsabilidades y cómo preparar tu cuenta.

9 min de lectura

Alternativa a Google Cloud IoT Core: opciones y migración tras su cierre

Google retiró Cloud IoT Core en agosto de 2023. Esta guía compara las alternativas realistas, traduce los conceptos de IoT Core a MQTT estándar y muestra cómo mover los dispositivos a un broker MQTT gestionado sin dejar de enviar datos a Google Cloud.

9 min de lectura

Mensajes MQTT por segundo: cómo estimar y medir el rendimiento

Planificar el rendimiento de MQTT se reduce a unas pocas fórmulas y una medición honesta. Aprende a estimar los mensajes entrantes y salientes por segundo, a tener en cuenta los picos y a trasladar el resultado a un plan de broker.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.