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.
| Perfil | Retardo (un sentido) | Jitter | Pérdida | Velocidad |
|---|---|---|---|---|
| LTE bueno | 40 ms | 10 ms | 0,1 % | 10 mbit |
| LTE débil | 120 ms | 40 ms | 1 % | 1 mbit |
| 3G | 200 ms | 50 ms | 1 % | 384 kbit |
| 2G / EDGE | 400 ms | 150 ms | 3 % | 64 kbit |
| Zona muerta | 1500 ms | 500 ms | 15 % | 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:
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# 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 normalnetem 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.
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")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 payloadsPublicador 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
8883con 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
| Ajuste | Comportamiento en celular | Test a ejecutar |
|---|---|---|
| QoS 0 | Menos bytes; lo que esté en tránsito al caer la conexión se pierde | Corta el enlace a mitad del lote y cuenta las pérdidas |
| QoS 1 | Un PUBACK extra por mensaje; se reintenta tras reconectar, posibles duplicados | El mismo corte, buscando duplicados |
| QoS 2 | Cuatro paquetes por mensaje, cada uno pagando el RTT | Compara el throughput con QoS 1 en 2G |
| Sesión persistente | Suscripciones y mensajes en cola sobreviven a las reconexiones | Desconecta 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
- Haz primero un smoke test en una red normal y luego aplica cada perfil netem.
- Registra por perfil el tiempo de conexión (con y sin TLS), el número de entregas y la latencia p50/p95.
- Encuentra el timeout de inactividad del NAT del operador con tests de inactividad + publish; pon el keep-alive por debajo.
- 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.
- Compara tamaños de payload y agrupación con el perfil 2G.
- 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.

