Many ESP32 projects run perfectly on a developer desk and collapse once deployed in apartments, offices, or industrial spaces with noisy Wi-Fi conditions. The main mistake is treating every disconnect as an exceptional event. In production, intermittent link issues are normal. Firmware should absorb them predictably.

1. Failure modes to design for

I model Wi-Fi failures in four classes:

  • short RF drop (seconds)
  • prolonged AP unavailability (minutes)
  • DHCP or DNS instability
  • credential or roaming mismatch

Each class needs a different recovery response. Using one generic reconnect loop is usually not enough.

2. Connection state machine

A robust ESP32 client should use explicit states:

  • BOOT
  • WIFI_CONNECTING
  • WIFI_ONLINE
  • WIFI_DEGRADED
  • OFFLINE_BUFFERING

Transitions should be driven by events and timers, not only callback side effects. This keeps behavior debuggable.

3. Backoff without reboot loops

Avoid immediate full-device resets after repeated failures. First apply reconnect attempts with exponential backoff and jitter:

  • attempts 1-3: short delay
  • attempts 4-10: medium delay
  • attempts >10: long delay and reduced network activity

Only perform controlled restart after a clearly defined threshold and with a reboot reason log.

4. Offline buffering policy

Telemetry should not be dropped immediately when offline. Keep a bounded ring buffer for latest payloads:

  • max entries by RAM budget
  • payload compaction for repeated metrics
  • include enqueue timestamp for staleness filtering

When connectivity returns, flush oldest-first with rate limiting.

5. Timeouts and watchdog strategy

Network calls must always have explicit timeout boundaries. Blocking forever on socket operations eventually deadlocks higher-level tasks.

Use watchdog supervision for the main loop and monitor task liveness counters. A watchdog should recover true stalls, not mask bad network logic.

6. Health signals for operators

Expose internal health indicators:

  • current Wi-Fi RSSI
  • reconnect count in current hour
  • queue depth
  • last successful publish timestamp

This allows remote diagnosis before users report outages.

7. Power interaction

Weak power rails often look like Wi-Fi instability. Brown-outs during TX bursts can mimic random disconnects. Add brown-out counter and measure voltage under peak transmission.

If battery-powered, align upload schedule with power budget and radio duty cycle.

8. Test matrix before deployment

Validate at minimum:

  • AP reboot while device running
  • DHCP server unavailable
  • intermittent packet loss
  • wrong credential fallback behavior
  • long offline period with buffered data

Most reconnection bugs are found only with these induced failures.

9. Concrete building blocks

A few Arduino-core / ESP-IDF primitives do most of the heavy lifting for the patterns above.

WiFi event callbacks. Drive the state machine from WiFi.onEvent() (which wraps the underlying esp_wifi / WiFiEventId_t events) rather than polling WiFi.status() in loop(). The disconnect event even carries a reason code that helps distinguish an AP reboot from a bad password:

WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      state = WIFI_ONLINE;
      backoffMs = 500;          // reset backoff on success
      break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      state = WIFI_CONNECTING;  // reason in info.wifi_sta_disconnected.reason
      break;
    default:
      break;
  }
});

Exponential backoff reconnect loop. The reconnect logic itself stays small and non-blocking, with a jittered, capped delay so a downed AP never turns into a tight retry storm:

void serviceWifi() {
  if (state == WIFI_ONLINE || millis() < nextAttempt) return;
  WiFi.disconnect();
  WiFi.begin(ssid, pass);
  backoffMs = min(backoffMs * 2, 60000UL);          // cap at 60 s
  nextAttempt = millis() + backoffMs + random(0, 500); // add jitter
}

Task Watchdog Timer. Subscribe the critical task to the TWDT and feed it only on a healthy pass through the loop. A true stall then triggers a clean, logged reset instead of an invisible hang:

esp_task_wdt_init(15, true);  // 15 s timeout, panic on expiry
esp_task_wdt_add(NULL);       // watch the current task
// ... inside the main loop, once work completes:
esp_task_wdt_reset();

NVS and RTC memory across resets. Small state that must survive a reboot goes in NVS (the Preferences library) — reconnect counters, last-known config, a reboot-reason string. For data that only needs to survive deep sleep, RTC_DATA_ATTR variables stay in RTC memory and cost far less than a flash write:

RTC_DATA_ATTR uint32_t bootCount = 0;   // survives deep sleep
Preferences prefs;
prefs.begin("net", false);
prefs.putUInt("reconnects", reconnectCount);  // survives full reset

Between them, the device can reboot, restore its buffering context, and pick up roughly where it left off instead of starting cold every time.

Final note

ESP32 reliability comes from controlled degradation and measured recovery. A good firmware keeps operating locally, buffers data safely, and reconnects without panicking the whole device.

Sources

  • Imported from the previous version of the site (gaborl.hu). Original publication around 2024-07-19.