Um cliente MQTT que é sólido como pedra no Wi-Fi do escritório pode desmoronar em um modem celular: conexões que dão timeout, desconexões por keep-alive a cada poucos minutos, mensagens QoS 1 que chegam em rajadas. A correção começa por reproduzir as condições celulares na sua mesa — latência, jitter, perda de pacotes e banda limitada — e depois ajustar keep-alive, tamanho de payload, QoS e sessões até o cliente aguentar.
Este guia mostra como emular links 2G, 3G e LTE com tc netem, quais configurações de MQTT testar em cada perfil e um script que mede entrega e latência para você comparar configurações.
O que torna o celular diferente
Links celulares não são só "Wi-Fi lento". Eles falham de maneiras específicas que atingem a conexão TCP de longa duração do MQTT:
- RTT alto e variável. Toda troca MQTT que espera resposta — CONNACK, SUBACK, PUBACK, PINGRESP — paga o RTT inteiro, e o jitter a torna imprevisível.
- Perdas em rajada. Handovers e sinal fraco derrubam vários pacotes seguidos. O TCP os retransmite, então o MQTT vê travamentos de segundos em vez de mensagens perdidas.
- Atraso para acordar o rádio. Depois de um período em silêncio o modem entra em um estado de rádio ocioso; o primeiro pacote seguinte espera o rádio reconectar, o que adiciona latência perceptível a essa mensagem.
- NAT da operadora (CGNAT). Operadoras móveis colocam os dispositivos atrás de gateways NAT que esquecem fluxos ociosos. Se nada passa pela conexão por mais tempo que o timeout de inatividade do NAT, ela fica half-open: o dispositivo acha que está conectado e o broker nunca mais tem notícias dele. Veja MQTT e timeouts de NAT.
- Dados tarifados. Cada byte de cabeçalho, keep-alive e handshake TLS custa dinheiro e bateria em uma frota de milhares de dispositivos.
Emulando links celulares com tc netem
No Linux, o tc com a disciplina de fila netem adiciona atraso, jitter, perda e limite de taxa a uma interface. Os perfis abaixo são pontos de partida aproximados para condições típicas, não medições de nenhuma rede específica — troque pelos números dos seus próprios dispositivos quando os tiver.
| Perfil | Atraso (um sentido) | Jitter | Perda | Taxa |
|---|---|---|---|---|
| LTE bom | 40 ms | 10 ms | 0,1 % | 10 mbit |
| LTE fraco | 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 morta | 1500 ms | 500 ms | 15 % | 16 kbit |
A configuração mais limpa é rodar o cliente MQTT dentro de um container e modelar só a rede desse container, sem mexer no 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 normalO netem modela apenas o tráfego de saída. Do lado do cliente isso basta para a maioria dos testes: o atraso é aplicado uma vez por ida e volta, e a perda de segmentos de saída dispara o mesmo comportamento de retransmissão do TCP. Se precisar modelar o downlink separadamente, coloque o netem do lado do broker de uma bridge ou use o Toxiproxy, que atrasa e limita cada direção de forma independente.
Confira o perfil antes de testar o MQTT: ping -c 10 broker.connectmq.com deve mostrar o atraso adicionado, e um mosquitto_pub ainda deve passar.
Medindo a entrega em cada perfil
Este script (paho-mqtt 2.x) conecta um subscriber e um publisher, envia um lote de mensagens QoS 1 e informa quantas chegaram, o tempo de conexão e os percentis de latência ponta a ponta. Rode dentro do container modelado uma 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 payloadsPublisher e subscriber compartilham o relógio do container, então não há erro de diferença de relógio nas latências. As duas conexões passam pela interface modelada, o que é pessimista — em produção o subscriber normalmente fica em um data center —, mas deixa bem visíveis as diferenças entre configurações. Mantenha o cliente MQTT online inscrito em testmqtt/cellular/# por uma conexão normal para ver as mensagens chegando.
Ajuste o keep-alive para a operadora, não para o laboratório
No celular, o keep-alive tem duas funções: manter viva a entrada do NAT e detectar links mortos. O intervalo de keep-alive precisa ser menor que o timeout de inatividade do NAT da operadora; caso contrário, a conexão morre em silêncio entre os pings.
Para descobrir esse timeout, conecte, fique totalmente ocioso (keep-alive maior que a janela do teste) e publique depois de pausas crescentes — 1, 2, 5, 10, 20 minutos. A primeira pausa depois da qual o publish nunca chega ao broker (e o cliente só percebe no próximo ping ou escrita) é o seu limite de NAT. Configure o keep-alive bem abaixo dele.
Teste também a falha oposta: nos perfis 2G e "zona morta", garanta que o keep-alive não cause desconexões. Se o PINGRESP frequentemente leva vários segundos, um cliente com timeouts agressivos pode derrubar conexões saudáveis. O MQTT permite que o broker espere uma vez e meia o keep-alive antes de desistir, então valores muito curtos (abaixo de 15–20 s) deixam pouca folga em links lentos.
Tamanho do payload e overhead do protocolo
Em um link de 64 kbit/s, uma mensagem de 20 KB ocupa o uplink por alguns segundos, e tudo que vem atrás espera. No perfil 2G, compare payloads pequenos e grandes com o script e você verá a latência p95 subir com o tamanho.
- Mantenha os payloads compactos. Chaves JSON curtas ou formatos binários como CBOR e Protobuf ajudam. A calculadora de tamanho de pacote mostra quanto o tamanho do topic e as propriedades somam por mensagem.
- Agrupe com critério. Uma mensagem com dez leituras tem menos overhead que dez mensagens, mas uma conexão perdida no meio do lote dói mais. Teste as duas abordagens.
- Conte o TLS. Um handshake TLS completo custa várias idas e voltas e alguns kilobytes. Em um link de 400 ms isso significa segundos por conexão, então reconectar com frequência sai caro. Meça o tempo de conexão com TLS na porta
8883em cada perfil e veja o overhead de banda do MQTT para os números por trás disso. - Use topic aliases do MQTT 5 se topics longos são enviados repetidamente, depois de confirmar que seu broker os suporta.
QoS e sessões em links com perda
| Configuração | Comportamento no celular | Teste a rodar |
|---|---|---|
| QoS 0 | Menos bytes; o que estiver em trânsito quando a conexão cai se perde | Corte o link no meio do lote e conte as perdas |
| QoS 1 | Um PUBACK extra por mensagem; reenviada após reconectar, duplicatas possíveis | Mesmo corte, procurando duplicatas |
| QoS 2 | Quatro pacotes por mensagem, cada um pagando o RTT | Compare o throughput com QoS 1 no 2G |
| Sessão persistente | Assinaturas e mensagens na fila sobrevivem às reconexões | Desconecte por 2 minutos, reconecte e conte a fila |
Para a maior parte da telemetria celular, QoS 1 com processamento idempotente no receptor é a escolha pragmática. Combine com uma sessão persistente — clean_start=False e um session expiry interval no MQTT 5 — para que o dispositivo não precise assinar de novo e as mensagens enfileiradas para ele sobrevivam a uma queda curta. Detalhes em sessões persistentes no MQTT.
Teste reconexões explicitamente nos perfis ruins: derrube o link (tc qdisc change dev eth0 root netem loss 100%), espere, restaure e verifique que o cliente reconecta com backoff em vez de martelar a rede, e que as mensagens QoS 1 publicadas durante a queda chegam quando o link volta.
Checklist de testes celulares
- Faça primeiro um smoke test em uma rede normal e depois aplique cada perfil netem.
- Registre por perfil o tempo de conexão (com e sem TLS), a contagem de entregas e a latência p50/p95.
- Descubra o timeout de inatividade do NAT da operadora com testes de ociosidade seguidos de publish; configure o keep-alive abaixo dele.
- Corte o link por 30 s e por 5 min; verifique o backoff de reconexão, a retomada de sessão e a reentrega QoS 1.
- Compare tamanhos de payload e agrupamento no perfil 2G.
- Repita os casos críticos em hardware real com o SIM de produção.
Redes celulares sempre serão mais lentas e menos previsíveis que o laboratório. Emulá-las a cada mudança transforma "funciona no escritório" em "funciona em um caminhão em movimento", e os números dessas execuções dão uma base sólida para suas decisões de keep-alive, QoS e payload.
Perguntas frequentes
Qual keep-alive usar para MQTT em rede celular?
Curto o bastante para ficar abaixo do timeout de inatividade do NAT da operadora e longo o bastante para não desperdiçar bateria e dados. Muitas implantações ficam entre 60 e 300 segundos, mas os timeouts de NAT variam por operadora: meça com o seu SIM o tempo ocioso depois do qual a conexão morre silenciosamente e configure o keep-alive bem abaixo disso.
Perda de pacotes causa perda de mensagens MQTT?
Não diretamente. O MQTT roda sobre TCP, que retransmite segmentos perdidos, então a perda aparece como picos de latência e travamentos. Mensagens se perdem quando a conexão cai: mensagens QoS 0 em trânsito somem, enquanto QoS 1 e 2 são reenviadas após a reconexão se a sessão persistir.
Posso emular condições celulares no macOS ou no Windows?
No macOS, o Network Link Conditioner da Apple aplica perfis de latência, perda e banda ao sistema inteiro. Em qualquer sistema você pode rodar o cliente dentro de um container Linux com tc netem, ou colocar o Toxiproxy entre cliente e broker para latência e limite de banda.

