Field note · Embedded debugging · Home network
Eero “not accepting connections”: fixing an ESP32 Wi-Fi reason=2 failure
A GPS tracker that had streamed telemetry for weeks suddenly refused to join the Wi-Fi — correct password, router a foot away, rejected every single time. Eight things later, the fix was one line, and the cause was the last thing a strong signal lets you suspect.
The short version
What broke: A little Wi-Fi gadget (an ESP32 microcontroller) stopped connecting to the home Eero mesh. The router kept rejecting it — even though the password was right, nothing had changed, and the router was inches away.
What actually fixed it: Turning the gadget’s Wi-Fi transmitter down. One line of code — WiFi.setTxPower(WIFI_POWER_8_5dBm) — dropped its transmit power from full blast to about a quarter of it, and it connected on the first try and has stayed connected ever since.
The counter-intuitive bit: The signal was excellent, so “not enough power” was obviously not the problem. It turned out the problem was too much power: pushed to maximum, right next to the router, the little radio’s signal gets distorted, and the router refuses the messy handshake. Backing it off cleans it up.
If your ESP32 throws reason=2 / “Auth Expired” with the correct password and a strong signal, lower its transmit power first. It costs one line and thirty seconds to test.
The full write-up
1 The system & the symptom
The device is a home-built GNSS tracker: an ESP32-S3 paired with a u-blox NEO-M9N receiver. Every 10 seconds it POSTs a JSON telemetry frame over Wi-Fi to a WordPress plugin that stores the track and renders a live dashboard. The network is an Eero mesh on WPA2. It had run reliably for weeks; the last good post was timestamped 21:16 the previous evening.
Then telemetry stopped. The serial console showed the device seeing the network, trying to join, and being deauthenticated ~2 seconds later — on every retry:
Every observable fact pointed away from the device:
- Credentials correct & unchanged — it had connected the night before.
- Signal excellent — the AP was ~1 ft away; a scan reported it at −30 dBm.
- Radio functional — receiving a
reason=2deauth at all proves the ESP32 transmitted a join request and received the AP’s reply.
reason=2 is WIFI_REASON_AUTH_EXPIRE (value 2 in esp_wifi_types.h) — the AP declaring the station’s authentication no longer valid and deauthenticating it. Correct password + strong signal + constant rejection is a genuinely confusing combination, which is why it took a full elimination pass to isolate.
2 The investigation — eliminate one cause at a time
Every hypothesis was tested on the actual hardware, and every one but the last returned the same reason=2. Recording the result of each is what let the process converge instead of circling.
| # | Hypothesis | What was done | Result |
|---|---|---|---|
| 1 | Firmware connection logic was wrong | Restored the exact working code, then tried 4 variants (auto-reconnect on/off, explicit begin(), reconnect(), back-off timing) | reason=2 |
| 2 | Router blacklisted our MAC | Assigned a fresh locally-administered MAC (verified by read-back) — a router can’t distinguish a new MAC from any new device | reason=2 |
| 3 | Router now requires WPA3 / PMF | Forced explicit WPA2/WPA3-SAE (Hash-to-Element) + PMF-capable | reason=2 |
| 4 | Corrupted Wi-Fi NVS / RF calibration | Full chip erase (esptool erase_flash) then reflashed clean | reason=2 |
| 5 | Marginal power browning out the radio | Disconnected the 2.8″ TFT (~80 mA of load removed) | reason=2 |
| 6 | Something specific to that network | Tried the main and guest SSIDs — two separate networks on the mesh | reason=2 |
| 7 | An Eero-side block / security setting | Checked device list (present, no block), confirmed WPA2, enabled the “legacy device” compatibility mode | reason=2 |
| ✓ | Transmit power was too high | Lowered TX power to 8.5 dBm via WiFi.setTxPower() | connected |
3 The red herring that cost the most time
The Eero app showed the tracker’s “last active” timestamp updating with each attempt — which looked exactly like the router seeing and accepting the device, pointing the whole investigation router-ward. In reality the ESP32 was associating for a split second, then being immediately deauthenticated. The mesh logged that fleeting handshake as activity; the rejection followed too fast to notice. A last-seen timestamp during a rapid connect/deauth loop is not evidence of success.
4 Root cause — the honest version
Verified fact: lowering the ESP32’s Wi-Fi transmit power fixed it, reproducibly and permanently. Leading explanation: at full power the transmitted signal is distorted enough that the AP rejects the handshake.
By default the ESP32 transmits at roughly 19.5 dBm. The widely-reported (and best-fitting) explanation is that on some boards the RF front-end is over-driven at maximum power — poor antenna/impedance matching or supply sag during transmit bursts degrades signal quality (EVM) enough that a fussy AP can’t cleanly complete authentication, and answers with a reason=2 deauth. Sitting a foot from the router at full power is close to the worst case for it.
AUTH_EXPIRE is a multi-cause error across the ESP32 family — others have traced the same code to router 802.11n handling, band-steering, or authentication-timing quirks, and fixed it by changing router settings or hardware rather than TX power. So the transmit-power mechanism above is the leading explanation, not a proven one. What is proven here is empirical: on this board, against this AP, dropping TX power was the single change that turned constant rejection into a stable connection.
5 The fix
Set the transmit power in Wi-Fi setup, and again right after begin() (which can reset it on some cores). Confirm it took by reading it back — setTxPower() is known to silently not apply on some builds.
// in wifiUplinkInit(), after WiFi.mode(WIFI_STA): WiFi.setTxPower(WIFI_POWER_8_5dBm); WiFi.begin(WIFI_SSID, WIFI_PASS); WiFi.setTxPower(WIFI_POWER_8_5dBm); // re-apply: begin() may reset it Serial.printf("txpower now %d\n", WiFi.getTxPower()); // verify
WiFi.setTxPower() takes one of a fixed ladder of levels — from WIFI_POWER_MINUS_1dBm up to WIFI_POWER_19_5dBm. getTxPower() returns the value in quarter-dBm units, so 8.5 dBm reads back as 34 (34 × 0.25). If 8.5 doesn’t do it on your board, step further down the ladder. At ~1 ft from the router, 8.5 dBm is still far more than the link needs.
6 Verification
Decisive on the first boot after the change — no reason=2, an immediate join, and a successful POST:
A 45-second stability check then showed a steady stream — one post every ~10 s, all HTTP 200, zero rejections:
The dashboard, frozen since the previous evening, went live again within seconds.
7 Takeaways
- For
reason=2/ AUTH_EXPIRE with a correct password and strong signal, lower the transmit power first. A one-line, thirty-second test that would have saved hours here. - A strong signal argues against the real cause. The failure comes from too much power, and proximity to the router makes it worse — the opposite of intuition.
- Distrust “the router accepted it” signals during a connect/deauth loop. A last-seen timestamp can be a split-second association, not a success.
- It’s multi-cause. If lowering TX power doesn’t fix your AUTH_EXPIRE, look at the router’s 802.11n / band-steering / WPA mode next — but try the free one-liner first.
- Eliminate methodically and log every result. The table is the method; it’s also what proved the fix rather than merely correlating with it.
Recent Comments