Pruebas de brokers

Probar MQTT en redes celulares lentas e inestables

Emula la latencia, el jitter y la pérdida de paquetes de 2G, 3G y LTE con tc netem y ajusta keep-alive, payload, QoS y sesiones MQTT para IoT móvil.

Actualizado · 9 min de lectura

Probar MQTT en redes celulares lentas e inestables

Un cliente MQTT sólido como una roca en el Wi-Fi de la oficina puede desmoronarse en un módem celular: conexiones que hacen timeout, desconexiones por keep-alive cada pocos minutos, mensajes QoS 1 que llegan a ráfagas. La solución empieza por reproducir las condiciones celulares en tu mesa —latencia, jitter, pérdida de paquetes y ancho de banda limitado— y luego ajustar keep-alive, tamaño de payload, QoS y sesiones hasta que el cliente las aguante.

Esta guía muestra cómo emular enlaces 2G, 3G y LTE con tc netem, qué ajustes de MQTT probar con cada perfil y un script que mide la entrega y la latencia para comparar configuraciones.

Qué hace diferente a la red celular

Los enlaces celulares no son simplemente "Wi-Fi lento". Fallan de maneras concretas que golpean la conexión TCP de larga duración de MQTT:

  • RTT alto y variable. Cada intercambio MQTT que espera respuesta —CONNACK, SUBACK, PUBACK, PINGRESP— paga el RTT completo, y el jitter lo vuelve impredecible.
  • Pérdidas a ráfagas. Los handovers y la señal débil tiran varios paquetes seguidos. TCP los retransmite, así que MQTT ve bloqueos de segundos en lugar de mensajes perdidos.
  • Retardo al despertar la radio. Tras un periodo de silencio el módem pasa a un estado de radio inactivo; el primer paquete después espera a que la radio se reconecte, lo que añade una latencia notable a ese mensaje.
  • NAT de operador (CGNAT). Los operadores móviles colocan los dispositivos detrás de gateways NAT que olvidan los flujos inactivos. Si nada cruza la conexión durante más tiempo que el timeout de inactividad del NAT, queda half-open: el dispositivo cree que está conectado y el broker no vuelve a saber de él. Consulta MQTT y los timeouts de NAT.
  • Datos tarificados. Cada byte de cabeceras, keep-alive y handshake TLS cuesta dinero y batería en una flota de miles de dispositivos.

Emular enlaces celulares con tc netem

En Linux, tc con la disciplina de colas netem añade retardo, jitter, pérdida y límites de velocidad a una interfaz. Los perfiles siguientes son puntos de partida aproximados para condiciones típicas, no mediciones de ninguna red concreta: sustitúyelos por cifras de tus propios dispositivos cuando las tengas.

PerfilRetardo (un sentido)JitterPérdidaVelocidad
LTE bueno40 ms10 ms0,1 %10 mbit
LTE débil120 ms40 ms1 %1 mbit
3G200 ms50 ms1 %384 kbit
2G / EDGE400 ms150 ms3 %64 kbit
Zona muerta1500 ms500 ms15 %16 kbit

La configuración más limpia es ejecutar el cliente MQTT dentro de un contenedor y modelar solo la red de ese contenedor, sin tocar el host:

A shaped test container
docker run --rm -it --cap-add NET_ADMIN --name cell python:3.12-slim bash

# inside the container
apt-get update && apt-get install -y iproute2 mosquitto-clients
pip install paho-mqtt
Apply and change profiles (inside the container)
# 3G-like: 200 ms ± 50 ms, 1 % loss with 25 % correlation (bursty), 384 kbit/s
tc qdisc add dev eth0 root netem delay 200ms 50ms loss 1% 25% rate 384kbit

tc qdisc change dev eth0 root netem delay 400ms 150ms loss 3% 25% rate 64kbit   # 2G
tc qdisc show dev eth0                                                         # verify
tc qdisc del dev eth0 root                                                     # back to normal

netem solo modela el tráfico saliente. En el lado del cliente basta para la mayoría de los tests: el retardo se aplica una vez por ida y vuelta, y la pérdida de segmentos salientes provoca el mismo comportamiento de retransmisión TCP. Si necesitas modelar el enlace descendente por separado, pon netem en el lado del broker de un bridge o usa Toxiproxy, que puede retrasar y limitar cada dirección de forma independiente.

Comprueba el perfil antes de probar MQTT: ping -c 10 broker.connectmq.com debería mostrar el retardo añadido, y un mosquitto_pub debería seguir pasando.

Medir la entrega con cada perfil

Este script (paho-mqtt 2.x) conecta un suscriptor y un publicador, envía un lote de mensajes QoS 1 e informa de cuántos llegaron, del tiempo de conexión y de los percentiles de latencia de extremo a extremo. Ejecútalo dentro del contenedor modelado una vez por perfil.

cell_test.py
import json, sys, threading, time, uuid
import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 1883
TOPIC = f"testmqtt/cellular/{uuid.uuid4().hex[:8]}"
N, QOS, SIZE = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])
latencies, done = [], threading.Event()

def on_message(client, userdata, msg):
    latencies.append((time.time() - json.loads(msg.payload)["t"]) * 1000)
    if len(latencies) == N:
        done.set()

def connect(name, keepalive=60):
    ready = threading.Event()
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"{name}-{uuid.uuid4().hex[:6]}")
    c.on_connect = lambda *args: ready.set()
    t0 = time.time()
    c.connect(HOST, PORT, keepalive=keepalive)
    c.loop_start()
    if not ready.wait(30):
        sys.exit(f"{name}: no CONNACK within 30 s")
    print(f"{name}: connected in {(time.time() - t0) * 1000:.0f} ms")
    return c

sub = connect("sub")
sub.on_message = on_message
sub.subscribe(TOPIC, qos=QOS)
pub = connect("pub")
time.sleep(2)

pad = "x" * SIZE
for i in range(N):
    pub.publish(TOPIC, json.dumps({"i": i, "t": time.time(), "pad": pad}), qos=QOS)
    time.sleep(0.2)

done.wait(60)
latencies.sort()
pct = lambda q: latencies[min(len(latencies) - 1, int(len(latencies) * q))]
print(f"received {len(latencies)}/{N} at QoS {QOS}, {SIZE}-byte padding")
if latencies:
    print(f"p50 {pct(0.5):.0f} ms  p95 {pct(0.95):.0f} ms  max {latencies[-1]:.0f} ms")
Run it
python cell_test.py 100 1 200    # 100 messages, QoS 1, ~200-byte payloads
python cell_test.py 100 0 200    # same at QoS 0 for comparison
python cell_test.py 20 1 20000   # few large payloads

Publicador y suscriptor comparten el reloj del contenedor, así que no hay error por desfase de relojes en las latencias. Ambas conexiones pasan por la interfaz modelada, lo que es pesimista —en producción el suscriptor suele estar en un centro de datos—, pero hace muy visibles las diferencias entre configuraciones. Mantén el cliente MQTT online suscrito a testmqtt/cellular/# desde una conexión normal para ver cómo van llegando los mensajes.

Ajusta el keep-alive al operador, no al laboratorio

En celular, el keep-alive tiene dos funciones: mantener viva la entrada del NAT y detectar enlaces muertos. El intervalo de keep-alive debe ser más corto que el timeout de inactividad del NAT del operador; si no, la conexión muere en silencio entre pings.

Para encontrar ese timeout, conéctate, permanece totalmente inactivo (keep-alive mayor que la ventana de prueba) y publica tras pausas crecientes: 1, 2, 5, 10, 20 minutos. La primera pausa tras la cual el publish nunca llega al broker (y el cliente solo se entera en el siguiente ping o escritura) es tu límite de NAT. Pon el keep-alive claramente por debajo.

Prueba también el fallo contrario: con los perfiles 2G y "zona muerta", asegúrate de que el keep-alive no provoca desconexiones. Si el PINGRESP tarda varios segundos con frecuencia, un cliente con timeouts agresivos puede tirar conexiones sanas. MQTT permite al broker esperar una vez y media el keep-alive antes de rendirse, así que valores muy cortos (menos de 15–20 s) dejan poco margen en enlaces lentos.

Tamaño del payload y overhead del protocolo

En un enlace de 64 kbit/s, un mensaje de 20 KB ocupa el uplink durante unos segundos y todo lo que va detrás espera. Con el perfil 2G, compara payloads pequeños y grandes con el script y verás cómo la latencia p95 sube con el tamaño.

  • Mantén los payloads compactos. Claves JSON cortas o formatos binarios como CBOR y Protobuf ayudan. La calculadora de tamaño de paquete muestra cuánto suman la longitud del topic y las propiedades por mensaje.
  • Agrupa con criterio. Un mensaje con diez lecturas tiene menos overhead que diez mensajes, pero una conexión perdida a mitad del lote duele más. Prueba ambas opciones.
  • Cuenta el TLS. Un handshake TLS completo cuesta varias idas y vueltas y varios kilobytes. En un enlace de 400 ms son segundos por conexión, así que reconectar a menudo sale caro. Mide el tiempo de conexión con TLS en el puerto 8883 con cada perfil, y consulta el overhead de ancho de banda de MQTT para ver las cifras de fondo.
  • Usa los topic aliases de MQTT 5 si envías topics largos repetidamente, tras comprobar que tu broker los soporta.

QoS y sesiones en enlaces con pérdidas

AjusteComportamiento en celularTest a ejecutar
QoS 0Menos bytes; lo que esté en tránsito al caer la conexión se pierdeCorta el enlace a mitad del lote y cuenta las pérdidas
QoS 1Un PUBACK extra por mensaje; se reintenta tras reconectar, posibles duplicadosEl mismo corte, buscando duplicados
QoS 2Cuatro paquetes por mensaje, cada uno pagando el RTTCompara el throughput con QoS 1 en 2G
Sesión persistenteSuscripciones y mensajes en cola sobreviven a las reconexionesDesconecta 2 minutos, reconecta y cuenta la cola

Para la mayoría de la telemetría celular, QoS 1 con procesamiento idempotente en el receptor es la opción pragmática. Combínalo con una sesión persistente —clean_start=False y un session expiry interval en MQTT 5— para que el dispositivo no tenga que volver a suscribirse y los mensajes encolados para él sobrevivan a un corte breve. Detalles en sesiones persistentes MQTT.

Prueba explícitamente las reconexiones con perfiles malos: tira el enlace (tc qdisc change dev eth0 root netem loss 100%), espera, restáuralo y comprueba que el cliente se reconecta con backoff en lugar de machacar la red, y que los mensajes QoS 1 publicados durante el corte llegan cuando vuelve el enlace.

Checklist de pruebas celulares

  1. Haz primero un smoke test en una red normal y luego aplica cada perfil netem.
  2. Registra por perfil el tiempo de conexión (con y sin TLS), el número de entregas y la latencia p50/p95.
  3. Encuentra el timeout de inactividad del NAT del operador con tests de inactividad + publish; pon el keep-alive por debajo.
  4. Corta el enlace 30 s y 5 min; verifica el backoff de reconexión, la reanudación de sesión y la reentrega QoS 1.
  5. Compara tamaños de payload y agrupación con el perfil 2G.
  6. Repite los casos críticos en hardware real con la SIM de producción.

Las redes celulares siempre serán más lentas e impredecibles que el laboratorio. Emularlas en cada cambio convierte "funciona en la oficina" en "funciona en un camión en movimiento", y las cifras de estas ejecuciones te dan una base sólida para decidir keep-alive, QoS y payloads.

Preguntas frecuentes

¿Qué keep-alive debo usar para MQTT sobre red celular?

Lo bastante corto para quedar por debajo del timeout de inactividad del NAT del operador y lo bastante largo para no gastar batería ni datos. Muchos despliegues acaban entre 60 y 300 segundos, pero los timeouts de NAT varían según el operador: mide con tu SIM el tiempo de inactividad tras el que la conexión muere en silencio y pon el keep-alive claramente por debajo.

¿La pérdida de paquetes provoca mensajes MQTT perdidos?

No directamente. MQTT va sobre TCP, que retransmite los segmentos perdidos, así que la pérdida se nota como picos de latencia y bloqueos. Los mensajes se pierden cuando cae la conexión: los QoS 0 en tránsito desaparecen, mientras que QoS 1 y 2 se reintentan tras reconectar si la sesión persiste.

¿Puedo emular condiciones celulares en macOS o Windows?

En macOS, el Network Link Conditioner de Apple aplica perfiles de latencia, pérdida y ancho de banda a todo el sistema. En cualquier sistema puedes ejecutar el cliente dentro de un contenedor Linux con tc netem, o poner Toxiproxy entre cliente y broker para latencia y límites de ancho de banda.