ESP32 MQTT reconnect logic is what separates a demo that works on your desk from a device that runs for months. In this guide you will replace the blocking reconnect() loop found in most examples with a non-blocking state machine that handles Wi-Fi and MQTT separately, backs off exponentially with jitter, resubscribes automatically and buffers readings while offline. At the end you will verify it the honest way: by rebooting your router while the ESP32 is connected to broker.connectmq.com.
Why the Classic Reconnect Loop Fails
Nearly every PubSubClient example contains some version of this:
void reconnect() {
while (!client.connected()) {
if (client.connect("esp32")) {
client.subscribe("cmd");
} else {
delay(5000);
}
}
}
It works until the network goes away. Then several problems appear at once:
| Problem | What happens in practice |
|---|---|
Infinite while loop |
Nothing else runs: buttons, sensors, displays and motor control all freeze until the broker is back |
delay(5000) |
Same as above, plus the task watchdog may fire in some configurations |
| No Wi-Fi check | The sketch hammers MQTT connects while Wi-Fi itself is down, each one waiting for DNS or TCP timeouts |
| Fixed retry interval | Hundreds of devices recovering from the same outage reconnect in lockstep and hit the broker at the same moment |
| Hard-coded client ID | Two boards running the same firmware kick each other off forever |
The fix is to treat connectivity as a background concern. loop() should run every few milliseconds no matter what, and connection attempts should happen only when they are due.
Understand PubSubClient's State Codes
Before improving anything, log client.state() on every failure. The codes tell you whether the problem is the network, the broker or your credentials.
| Code | Constant | Meaning | Typical cause |
|---|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT |
No response within keep-alive | Network stalled, weak Wi-Fi |
| -3 | MQTT_CONNECTION_LOST |
Connection dropped | Router reboot, broker restart, duplicate client ID |
| -2 | MQTT_CONNECT_FAILED |
TCP connect failed | Wrong host or port, DNS failure, no internet |
| -1 | MQTT_DISCONNECTED |
Cleanly disconnected | You called disconnect() |
| 1 | MQTT_CONNECT_BAD_PROTOCOL |
Protocol not supported | Rare with 3.1.1 brokers |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID |
Client ID rejected | Empty or invalid ID |
| 3 | MQTT_CONNECT_UNAVAILABLE |
Server unavailable | Broker overloaded or restarting |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS |
Bad username/password | Typo, rotated password |
| 5 | MQTT_CONNECT_UNAUTHORIZED |
Not authorized | Credentials not allowed for this client ID |
Codes 4 and 5 are not transient. Retrying every few seconds will not fix them and only adds failed logins. Many brokers, ConnectMQ included, block IP addresses after repeated failed logins, so a device that loops on bad credentials can lock itself out. The firmware below backs off much harder on those codes. If you get -2 from the very first attempt, the problem is not reconnection at all; work through why an ESP32 cannot connect to the MQTT broker first.
Design of a Non-Blocking ESP32 MQTT Reconnect
The firmware separates three layers:
- Wi-Fi. Let the ESP32's own auto-reconnect handle the radio, and listen to Wi-Fi events to know when the link is up. Do not call
WiFi.begin()in a tight loop. - MQTT. Only attempt a connection when Wi-Fi has an IP address and the backoff timer has expired. On success, reset the backoff, publish an
onlinestatus and resubscribe. On failure, double the delay up to a cap and add random jitter. - Application. Read sensors and handle actuators every loop, regardless of connectivity. Publish when connected; otherwise put readings into a small ring buffer and flush them after reconnecting.
Exponential backoff with jitter is the standard pattern for retrying against a shared service. The jitter matters more than people expect: after a regional outage, it spreads thousands of reconnects over a window instead of a single spike.
The Complete Sketch
#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; // built from the MAC address
String base; // connectmq/demo/<clientId>
WiFiClient net;
PubSubClient mqtt(net);
// ---- Backoff state ----
const uint32_t BACKOFF_MIN = 1000; // 1 s
const uint32_t BACKOFF_MAX = 60000; // 60 s
const uint32_t BACKOFF_AUTH = 300000; // 5 min for credential errors
uint32_t backoff = BACKOFF_MIN;
unsigned long nextAttempt = 0;
volatile bool wifiUp = false;
uint32_t reconnects = 0;
// ---- Offline ring buffer ----
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() {
// Called after EVERY successful connect: subscriptions do not survive
// a clean-session reconnect.
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; // buffer while offline
head = (head + 1) % BUF_SIZE;
if (count < BUF_SIZE) count++; // oldest entry is overwritten when full
}
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); // limits how long a stalled connect can block
}
unsigned long lastSample = 0;
void loop() {
maintainMqtt();
// Application work always runs, connected or not
if (millis() - lastSample >= 10000) {
lastSample = millis();
String json = "{\"uptime\":" + String(millis() / 1000) +
",\"rssi\":" + String(WiFi.RSSI()) +
",\"reconnects\":" + String(reconnects) + "}";
publishReading(json);
}
}
Notes on the important lines
- Client ID from the MAC address. Every board gets a stable, unique ID without editing the sketch. Duplicate IDs are the number one cause of mysterious
-3loops, because the broker disconnects the older session whenever the newer one connects. With portal credentials, use the client ID the portal issued instead, since ConnectMQ binds client IDs to credentials. subscribeAll()after every connect. PubSubClient connects with a clean session, so the broker forgets subscriptions when the connection drops. Forgetting to resubscribe is the classic "it reconnects but stops receiving commands" bug.- Retained
online/offlinestatus. The Last Will tells everyone the device dropped; the retainedonlinecorrects it after recovery. The ESP32 MQTT Last Will guide covers this pattern in depth. setSocketTimeout(5). Even a non-blocking design blocks during the actual connect call, because DNS lookup, the TCP handshake and the CONNACK wait are synchronous in PubSubClient. Keeping the timeout short bounds that pause.- Rollover-safe timer.
(long)(millis() - nextAttempt) < 0keeps working whenmillis()wraps after roughly 49 days.
Test It: Reboot the Router
A reconnect strategy you have not tested is a hope, not a strategy. Here is a test plan that takes ten minutes:
- Flash the sketch and open the serial monitor.
- On your computer, subscribe to the device's topics (replace the ID with the one printed on the serial monitor):
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-test01 -v -t "connectmq/demo/esp32-a1b2c3/#"
- Confirm you see
status onlineand a telemetry message every ten seconds. - Power-cycle your router. Your computer loses its connection too, so watch the serial monitor instead.
- Within about 1.5 times the keep-alive (45 seconds here) the broker publishes the Last Will. You will see it once your laptop reconnects.
- Watch the serial log:
Wi-Fi lost, then nothing while the router boots (no failed MQTT attempts, because Wi-Fi is down), thenWi-Fi up, thenMQTT connected (#2). - On the subscriber, verify that
status onlinereturns, buffered telemetry arrives in a burst, andreconnectshas increased by one. - Send a command to
connectmq/demo/esp32-a1b2c3/cmd/testand confirm the device still receives it. That proves resubscription works.
Repeat the test with the broker unreachable but Wi-Fi up, for example by temporarily changing MQTT_HOST to an unused name. You should see retry intervals grow from about one second to the 60-second cap, with slightly different values each time because of the jitter.
Troubleshooting Reconnect Problems
- Reconnects every few seconds with state -3. Another device or a forgotten test client uses the same client ID.
- Connected, but commands stop arriving after a reconnect. Missing resubscribe.
- State -4 on a weak signal. Increase keep-alive moderately, improve antenna placement, and log RSSI as shown.
- Device never recovers after a long outage without a reboot. Something blocks forever, often an HTTP call or a sensor library. Add the task watchdog, as described in ESP32 task watchdog with MQTT.
- Burst of old messages surprises the dashboard. Include a timestamp in buffered readings so consumers can tell late data from live data.
When the Broker Side Matters
Good firmware assumes the broker can be unreachable, but how often that happens depends on where the broker runs. A Mosquitto instance on a home server goes down with the power, the OS update and the expired certificate. A managed MQTT broker moves those concerns to a team that operates the service for you. ConnectMQ also exposes $SYS/broker/connection/{clientId}/state, so a backend can watch a device's connection state, and the portal's live topic explorer lets you watch retained status topics flip between online and offline while you test. For background on keep-alive, sessions and the Last Will, the overview of what an MQTT broker does is a useful companion. Testing with the public MQTT test broker is fine for this exercise; the same firmware moves to private credentials and TLS by changing three constants and the client class, as shown in ESP32 MQTT over SSL/TLS. For the broader ESP32 journey, see the ESP32 IoT tutorial.
Get Started with ConnectMQ
Create a free Serverless broker at app.connectmq.com, no credit card required. You get dedicated credentials, TLS on port 8883 and a portal that shows when each device connects and disconnects, which is exactly what you need when tuning reconnect behaviour.
Frequently Asked Questions
How often should an ESP32 retry an MQTT connection?
Start around one second and double the wait after each failure up to a cap of about a minute, with random jitter added. For credential errors, wait much longer or stop and report the problem, because retries cannot succeed.
Do I need to resubscribe after an ESP32 MQTT reconnect?
With PubSubClient, yes. It uses a clean session, so the broker drops subscriptions when the connection ends. Call your subscribe function after every successful connect.
Should I call WiFi.begin() again when Wi-Fi drops?
Usually not. With WiFi.setAutoReconnect(true) the ESP32 reconnects on its own. Calling WiFi.begin() repeatedly can interrupt an association that is already in progress.
Why does my ESP32 keep disconnecting with state -3?
The most common cause is two clients sharing one client ID; the broker disconnects the older one each time the other connects. Weak Wi-Fi and a keep-alive that is longer than a NAT or firewall idle timeout are the next suspects.



