La lógica de reconexión MQTT en el ESP32 es lo que separa una demo que funciona en tu escritorio de un dispositivo que funciona durante meses. En esta guía sustituirás el bucle bloqueante reconnect() que aparece en la mayoría de ejemplos por una máquina de estados sin bloqueos que gestiona Wi-Fi y MQTT por separado, aplica backoff exponencial con jitter, se vuelve a suscribir automáticamente y guarda las lecturas en un búfer mientras está offline. Al final lo verificarás de la forma honesta: reiniciando tu router mientras el ESP32 está conectado a broker.connectmq.com.
Por qué falla el bucle de reconexión clásico
Casi todos los ejemplos de PubSubClient contienen alguna versión de esto:
void reconnect() {
while (!client.connected()) {
if (client.connect("esp32")) {
client.subscribe("cmd");
} else {
delay(5000);
}
}
}
Funciona hasta que la red desaparece. Entonces aparecen varios problemas a la vez:
| Problema | Qué ocurre en la práctica |
|---|---|
Bucle while infinito |
No se ejecuta nada más: botones, sensores, pantallas y control de motores se congelan hasta que el broker vuelve |
delay(5000) |
Lo mismo, y además el task watchdog puede saltar en algunas configuraciones |
| Sin comprobación de Wi-Fi | El sketch machaca con conexiones MQTT mientras el propio Wi-Fi está caído, y cada una espera a los timeouts de DNS o TCP |
| Intervalo de reintento fijo | Cientos de dispositivos que se recuperan del mismo corte se reconectan al unísono y golpean el broker en el mismo instante |
| Client ID fijo en el código | Dos placas con el mismo firmware se expulsan mutuamente sin fin |
La solución es tratar la conectividad como una tarea de fondo. loop() debe ejecutarse cada pocos milisegundos pase lo que pase, y los intentos de conexión solo deben producirse cuando toca.
Entiende los códigos de estado de PubSubClient
Antes de mejorar nada, registra client.state() en cada fallo. Los códigos te dicen si el problema es la red, el broker o tus credenciales.
| Código | Constante | Significado | Causa típica |
|---|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT |
Sin respuesta dentro del keep-alive | Red atascada, Wi-Fi débil |
| -3 | MQTT_CONNECTION_LOST |
Conexión perdida | Reinicio del router, reinicio del broker, client ID duplicado |
| -2 | MQTT_CONNECT_FAILED |
Falló la conexión TCP | Host o puerto incorrectos, fallo de DNS, sin internet |
| -1 | MQTT_DISCONNECTED |
Desconexión limpia | Llamaste a disconnect() |
| 1 | MQTT_CONNECT_BAD_PROTOCOL |
Protocolo no soportado | Raro con brokers 3.1.1 |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID |
Client ID rechazado | ID vacío o no válido |
| 3 | MQTT_CONNECT_UNAVAILABLE |
Servidor no disponible | Broker sobrecargado o reiniciándose |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS |
Usuario/contraseña incorrectos | Errata, contraseña rotada |
| 5 | MQTT_CONNECT_UNAUTHORIZED |
No autorizado | Credenciales no permitidas para este client ID |
Los códigos 4 y 5 no son transitorios. Reintentar cada pocos segundos no los arreglará y solo suma inicios de sesión fallidos. Muchos brokers, ConnectMQ incluido, bloquean direcciones IP tras repetidos inicios de sesión fallidos, así que un dispositivo que entra en bucle con credenciales incorrectas puede bloquearse a sí mismo. El firmware de abajo aplica un backoff mucho más agresivo con esos códigos. Si obtienes -2 desde el primerísimo intento, el problema no es la reconexión en absoluto; revisa primero por qué un ESP32 no puede conectarse al broker MQTT.
Diseño de una reconexión MQTT sin bloqueos en el ESP32
El firmware separa tres capas:
- Wi-Fi. Deja que la reconexión automática propia del ESP32 gestione la radio y escucha los eventos de Wi-Fi para saber cuándo el enlace está activo. No llames a
WiFi.begin()en un bucle cerrado. - MQTT. Intenta conectar solo cuando el Wi-Fi tenga dirección IP y haya expirado el temporizador de backoff. Si tiene éxito, reinicia el backoff, publica un estado
onliney vuelve a suscribirte. Si falla, duplica la espera hasta un tope y añade jitter aleatorio. - Aplicación. Lee sensores y gestiona actuadores en cada iteración, haya o no conectividad. Publica cuando estés conectado; si no, guarda las lecturas en un pequeño búfer circular y vacíalo tras reconectar.
El backoff exponencial con jitter es el patrón estándar para reintentar contra un servicio compartido. El jitter importa más de lo que se suele pensar: tras un corte regional, reparte miles de reconexiones en una ventana de tiempo en lugar de un único pico.
El sketch completo
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
String clientId; // construido a partir de la dirección MAC
String base; // connectmq/demo/<clientId>
WiFiClient net;
PubSubClient mqtt(net);
// ---- Estado del backoff ----
const uint32_t BACKOFF_MIN = 1000; // 1 s
const uint32_t BACKOFF_MAX = 60000; // 60 s
const uint32_t BACKOFF_AUTH = 300000; // 5 min para errores de credenciales
uint32_t backoff = BACKOFF_MIN;
unsigned long nextAttempt = 0;
volatile bool wifiUp = false;
uint32_t reconnects = 0;
// ---- Búfer circular offline ----
const int BUF_SIZE = 20;
String pending[BUF_SIZE];
int head = 0, count = 0;
void onWiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiUp = true;
Serial.printf("Wi-Fi up, IP %s\n", WiFi.localIP().toString().c_str());
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
if (wifiUp) Serial.println("Wi-Fi lost");
wifiUp = false;
break;
default:
break;
}
}
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.printf("cmd on %s: %.*s\n", topic, (int)length, (const char*)payload);
}
void subscribeAll() {
// Se llama tras CADA conexión correcta: las suscripciones no sobreviven
// a una reconexión con clean session.
mqtt.subscribe((base + "/cmd/#").c_str(), 1);
}
void flushPending() {
while (count > 0 && mqtt.connected()) {
int tail = (head - count + BUF_SIZE) % BUF_SIZE;
if (!mqtt.publish((base + "/telemetry").c_str(), pending[tail].c_str())) break;
count--;
mqtt.loop();
}
}
void scheduleRetry(int state) {
bool authError = (state == MQTT_CONNECT_BAD_CREDENTIALS || state == MQTT_CONNECT_UNAUTHORIZED);
uint32_t wait = authError ? BACKOFF_AUTH : backoff;
uint32_t jitter = random(0, wait / 4 + 1);
nextAttempt = millis() + wait + jitter;
if (!authError) backoff = min(backoff * 2, BACKOFF_MAX);
Serial.printf("MQTT connect failed (state %d), retry in %lu ms\n", state, (unsigned long)(wait + jitter));
}
void maintainMqtt() {
if (mqtt.connected()) {
mqtt.loop();
return;
}
if (!wifiUp || (long)(millis() - nextAttempt) < 0) return;
String status = base + "/status";
bool ok = mqtt.connect(clientId.c_str(), nullptr, nullptr,
status.c_str(), 1, true, "offline");
if (!ok) {
scheduleRetry(mqtt.state());
return;
}
reconnects++;
backoff = BACKOFF_MIN;
Serial.printf("MQTT connected (#%lu)\n", (unsigned long)reconnects);
mqtt.publish(status.c_str(), "online", true);
subscribeAll();
flushPending();
}
void publishReading(const String& json) {
if (mqtt.connected() && mqtt.publish((base + "/telemetry").c_str(), json.c_str())) return;
pending[head] = json; // guardar en búfer mientras está offline
head = (head + 1) % BUF_SIZE;
if (count < BUF_SIZE) count++; // la entrada más antigua se sobrescribe cuando está lleno
}
void setup() {
Serial.begin(115200);
uint8_t mac[6];
WiFi.mode(WIFI_STA);
WiFi.macAddress(mac);
char id[24];
snprintf(id, sizeof(id), "esp32-%02x%02x%02x", mac[3], mac[4], mac[5]);
clientId = id;
base = "connectmq/demo/" + clientId;
randomSeed(esp_random());
WiFi.onEvent(onWiFiEvent);
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setSocketTimeout(5); // limita cuánto puede bloquear una conexión atascada
}
unsigned long lastSample = 0;
void loop() {
maintainMqtt();
// El trabajo de la aplicación siempre se ejecuta, con o sin conexión
if (millis() - lastSample >= 10000) {
lastSample = millis();
String json = "{\"uptime\":" + String(millis() / 1000) +
",\"rssi\":" + String(WiFi.RSSI()) +
",\"reconnects\":" + String(reconnects) + "}";
publishReading(json);
}
}
Notas sobre las líneas importantes
- Client ID a partir de la dirección MAC. Cada placa obtiene un ID estable y único sin editar el sketch. Los IDs duplicados son la causa número uno de bucles misteriosos con
-3, porque el broker desconecta la sesión más antigua cada vez que se conecta la nueva. Con credenciales del portal, usa en su lugar el client ID que emitió el portal, ya que ConnectMQ vincula los client IDs a las credenciales. subscribeAll()tras cada conexión. PubSubClient se conecta con clean session, así que el broker olvida las suscripciones cuando se cae la conexión. Olvidar volver a suscribirse es el bug clásico de "se reconecta pero deja de recibir comandos".- Estado
online/offlineretenido. El Last Will avisa a todos de que el dispositivo se cayó; elonlineretenido lo corrige tras la recuperación. La guía de Last Will MQTT en ESP32 cubre este patrón en profundidad. setSocketTimeout(5). Incluso un diseño sin bloqueos se bloquea durante la llamada de conexión en sí, porque la resolución DNS, el handshake TCP y la espera del CONNACK son síncronos en PubSubClient. Mantener el timeout corto acota esa pausa.- Temporizador seguro frente al desbordamiento.
(long)(millis() - nextAttempt) < 0sigue funcionando cuandomillis()da la vuelta tras unos 49 días.
Pruébalo: reinicia el router
Una estrategia de reconexión que no has probado es una esperanza, no una estrategia. Aquí tienes un plan de pruebas que lleva diez minutos:
- Flashea el sketch y abre el monitor serie.
- En tu equipo, suscríbete a los topics del dispositivo (sustituye el ID por el que aparece en el monitor serie):
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-test01 -v -t "connectmq/demo/esp32-a1b2c3/#"
- Confirma que ves
status onliney un mensaje de telemetría cada diez segundos. - Apaga y enciende el router. Tu equipo también pierde la conexión, así que observa el monitor serie.
- En aproximadamente 1,5 veces el keep-alive (45 segundos aquí) el broker publica el Last Will. Lo verás en cuanto tu portátil se reconecte.
- Observa el log serie:
Wi-Fi lost, luego nada mientras arranca el router (sin intentos MQTT fallidos, porque el Wi-Fi está caído), luegoWi-Fi upy despuésMQTT connected (#2). - En el suscriptor, verifica que vuelve
status online, que la telemetría almacenada llega de golpe y quereconnectsha aumentado en uno. - Envía un comando a
connectmq/demo/esp32-a1b2c3/cmd/testy confirma que el dispositivo todavía lo recibe. Eso demuestra que la resuscripción funciona.
Repite la prueba con el broker inaccesible pero el Wi-Fi activo, por ejemplo cambiando temporalmente MQTT_HOST por un nombre no utilizado. Deberías ver cómo los intervalos de reintento crecen desde aproximadamente un segundo hasta el tope de 60 segundos, con valores ligeramente distintos cada vez por el jitter.
Solución de problemas de reconexión
- Se reconecta cada pocos segundos con estado -3. Otro dispositivo o un cliente de prueba olvidado usa el mismo client ID.
- Conectado, pero los comandos dejan de llegar tras una reconexión. Falta la resuscripción.
- Estado -4 con señal débil. Aumenta moderadamente el keep-alive, mejora la colocación de la antena y registra el RSSI como se muestra.
- El dispositivo nunca se recupera tras un corte largo sin un reinicio. Algo se bloquea para siempre, a menudo una llamada HTTP o una librería de sensores. Añade el task watchdog, como se describe en task watchdog del ESP32 con MQTT.
- Una ráfaga de mensajes antiguos sorprende al dashboard. Incluye una marca de tiempo en las lecturas almacenadas para que los consumidores distingan los datos tardíos de los datos en vivo.
Cuando importa el lado del broker
Un buen firmware asume que el broker puede ser inaccesible, pero la frecuencia con que eso ocurre depende de dónde se ejecute el broker. Una instancia de Mosquitto en un servidor doméstico cae con un corte de luz, una actualización del sistema operativo o un certificado caducado. Un broker MQTT gestionado traslada esas preocupaciones a un equipo que opera el servicio por ti. ConnectMQ además expone $SYS/broker/connection/{clientId}/state, de modo que un backend puede vigilar el estado de conexión de un dispositivo, y el explorador de topics en vivo del portal te permite ver cómo los topics de estado retenidos cambian entre online y offline mientras pruebas. Para entender el keep-alive, las sesiones y el Last Will, la introducción a qué hace un broker MQTT es un buen complemento. Para este ejercicio basta con probar en el servidor MQTT público gratuito; el mismo firmware pasa a credenciales privadas y TLS cambiando tres constantes y la clase del cliente, como se muestra en MQTT sobre SSL/TLS en ESP32. Para el recorrido completo con el ESP32, consulta el tutorial de IoT con ESP32.
Empieza con ConnectMQ
Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito. Obtienes credenciales dedicadas, TLS en el puerto 8883 y un portal que muestra cuándo se conecta y desconecta cada dispositivo, justo lo que necesitas al ajustar el comportamiento de reconexión.
Preguntas frecuentes
¿Con qué frecuencia debe un ESP32 reintentar una conexión MQTT?
Empieza en torno a un segundo y duplica la espera tras cada fallo hasta un tope de aproximadamente un minuto, añadiendo jitter aleatorio. Ante errores de credenciales, espera mucho más o detente y notifica el problema, porque los reintentos no pueden tener éxito.
¿Tengo que volver a suscribirme tras una reconexión MQTT del ESP32?
Con PubSubClient, sí. Usa clean session, así que el broker descarta las suscripciones cuando termina la conexión. Llama a tu función de suscripción tras cada conexión correcta.
¿Debo llamar otra vez a WiFi.begin() cuando se cae el Wi-Fi?
Normalmente no. Con WiFi.setAutoReconnect(true) el ESP32 se reconecta por sí solo. Llamar a WiFi.begin() repetidamente puede interrumpir una asociación que ya está en curso.
¿Por qué mi ESP32 se desconecta continuamente con estado -3?
La causa más habitual son dos clientes que comparten un mismo client ID; el broker desconecta al más antiguo cada vez que el otro se conecta. Los siguientes sospechosos son un Wi-Fi débil y un keep-alive más largo que el timeout de inactividad de un NAT o un firewall.



