The battery / deep-sleep layer of esp32. Every ESP32 project in this vault so far — lightaim, hardcut, 2026-08-11-ruview-esp32-csi-real-hardware — is mains- or vehicle-powered and always-on, so none of these rules has ever been exercised here. They are silicon and component facts, true regardless of what any one battery project turns out to be.

For Agents

Continuation of esp32’s hazard index, same rule: every entry carries the symptom. What unites them is that they are all invisible on a bench with USB plugged in — they surface weeks later as “it was dead when I went to use it.”

Index of hazards — the ones that look healthy while broken: 12. A DevKit carrier board on a battery → 20–45 mA while “asleep”, ~3000× the bare module (DevKit carrier boards destroy any battery budget) 13. Planning a GPIO wake on an ESP8266 → the silicon cannot do it, at all (ESP8266 cannot wake from deep sleep on a GPIO) 14. A WS2812 left powered while showing black → ~1 mA, ~100× the entire sleep budget (WS2812 draws ~1 mA while showing black) 15. A power-bank / boost module as the supply → it switches itself off ~30 s after the device sleeps (Power-bank and boost modules auto-shut-off at low load) 16. An LDO chosen on output current, not quiescent → the regulator outdraws the sleeping MCU (LDO quiescent current can exceed the MCU’s sleep draw) 17. Chasing µA below ~10 µA → unmeasurable; self-discharge already dominates (Li-ion self-discharge is the floor) 18. A normally-closed switch on an internal pull-up → 73 µA burning 24/7 (A normally-closed switch bleeds current continuously) 19. ext1 ANY_LOW entered while a masked pin is already low → infinite wake loop, flat cell in hours (ext1 ANY_LOW wake loop) 20. ⚠️ Trusting the ~7 µA figure on an N16R8 → unverified for octal PSRAM (Octal PSRAM deep-sleep adder — MEASURE) 21. Naive WiFi wake path → ~2.6 s awake per event where ~500 ms is achievable (WiFi wake-to-delivered latency is mostly avoidable)

Confidence Key

Every figure below is tagged, because the difference between “datasheet” and “someone measured it once” decides whether you can design on it.

MarkMeaning
Datasheet or well-established silicon behaviour — design on it
🔶Characteristic of the part class; the specific unit still needs the bench test named in the entry
⚠️ MEASUREUnverified. Do not put it in a budget until a meter has seen it

The Budget

Reference cell throughout: a single 18650 at 2 800 mAh. Everything is quoted against it because it makes the ratios visceral.

DrawSource18650 endurance
~1 µAnRF52840 deep sleep ✅(irrelevant — see )
~3.3 µANC switch done right (1 MΩ pull-down) ✅
~5 µATP4056 + DW01A protection standby ✅
~7–10 µABare ESP32-S3-WROOM-1 deep sleep~30 years
~20 µAESP8266EX deep sleep ✅~16 years
~55 µAAP2112K-3.3 quiescent ✅
~73 µANC switch on the internal pull-up ✅
~78 µA equivalentLi-ion self-discharge, 2 %/month~4 years — the hard floor
~0.4–0.9 mAESP8266 light sleep ✅~130 days
~0.7–1 mAWS2812 showing black~4 months
20–45 mAESP32-S3 DevKit carrier board, “deep sleep”~4 days
~350–500 mAESP32 WiFi TX peak ✅(peak, not average — but it sets the regulator)

Two numbers carry the whole section

Self-discharge ≈ 1.9 mAh/day. A sleeping bare S3 ≈ 0.24 mAh/day. The cell leaks ~8× faster than the MCU consumes. Once the device is under ~10 µA, every remaining hazard on this page is a board-level or component-level mistake, not a firmware one.

Hazards

DevKit carrier boards destroy any battery budget

The carrier board decides the sleep current, not the chip — a ~3000× spread

Deep-sleep draw18650
Bare ESP32-S3-WROOM-1 module~7–10 µA~30 years
DevKit carrier: CH343 USB-UART + AMS1117 LDO + power LED20–45 mA~4 days

None of that is the ESP32. It is the always-on bridge chip, a high-quiescent LDO and an LED nobody asked for, all sitting upstream of anything firmware can switch off. esp_deep_sleep_start() returns, the chip genuinely sleeps, the board still eats 30 mA. ✅

Consequence for the stock in Boards on Hand:

StockBattery verdict
10× ESP32-S3-WROOM-1-N16R8-M — bare modules✅ usable (subject to )
5× ESP32-DEVKIT-32UE-4M — DevKitsunusable for battery work. Keep them for their U.FL connector, which is the scarce thing about them

Bench implication: a DevKit is still the right thing to develop on. Just never conclude anything about power from one — the measurement has to happen on the bare module, on its own carrier.

ESP8266 cannot wake from deep sleep on a GPIO

There is no ext0/ext1 equivalent in ESP8266EX. Not "awkward" — absent

The only deep-sleep wake source is the RTC timer, routed out of GPIO16 into RST. A button cannot wake it. ✅

Both workarounds are worse than they look:

(a) Button wired to RST. A reset carries no record of which pin caused it — with more than one button you cannot tell them apart. Worse, the ESP8266 takes ~250 ms to reach user code, which is longer than a quick tap, so it boots to find the pin already released and the press vanishes. Fixing that needs an external latch per button — a 74HC74 D flip-flop that holds the press until firmware reads and clears it. (Same part already on the bench for a different reason: hardcut-bench-inventory has 2× 74HC74, and 2026-07-29-hardcut-design uses one as a hardware interlock.)

(b) Light sleep, which does wake on GPIO — at 0.4–0.9 mA. That takes an 18650 from years to ~130 days.

There is no power win hiding behind the inconvenience

ESP8266 deep sleep is ~20 µAworse than the ESP32-S3’s 7–10 µA. You would be accepting a latch IC per button and a wake source that cannot identify itself, in exchange for more current. If a design needs GPIO wake, it needs an ESP32.

WS2812 draws ~1 mA while showing black

"All pixels off" is not off

A WS2812/NeoPixel’s controller die is powered whenever VDD is present. Idle draw is ~0.7–1 mA per pixel even at RGB(0,0,0) ✅ — roughly 100× a deep-sleeping ESP32’s entire budget. An 18650 goes from years to about four months, and to weeks once you add any wake duty cycle.

Fixes, in order of preference:

  1. Plain LEDs on GPIO. A GPIO driven low sinks nothing. If the UI is one or two indicators, the addressable pixel buys nothing.
  2. Gate VDD through a MOSFET. A high-side P-FET (or low-side N-FET on the pixel’s ground, if the data line can be held low too) removes the pixel from the budget entirely while asleep. Note the pixel needs a few hundred µs after power-up before it will latch data.

Do not rely on writing black. Writing black is a display state; it is not a power state.

Power-bank and boost modules auto-shut-off at low load

Wrong class of part. It will make a working device look dead

Power-bank ICs are designed to shut their output off when nothing is drawing — that is the feature that stops a phone charger flattening itself in a drawer. A device sleeping at 10 µA is indistinguishable from “nothing plugged in”, so the module kills its own output within ~30 seconds and never comes back until the button is pressed. 🔶

Threshold for this class is around 40–50 mA. Well documented on the IP5306; the MH-CD42 (Type-C in, 18650 charge, boosted 5 V out, 4 level LEDs, button) is the same class of part.

Two more problems stack on top even if the shut-off could be defeated:

  • Double conversion. 3.7 V cell → boost to 5 V → drop back to 3.3 V loses 15–25 % for nothing. The cell is already at a usable voltage.
  • The boost converter’s own quiescent draw is milliamps — by itself enough to swamp the MCU budget.

Bench test — the 470 Ω trick

Hang a 470 Ω resistor across the 5 V output (≈ 10 mA) and come back after a minute.

  • Still on → this specific unit does not auto-shut-off at 10 mA. Retest lower before trusting it.
  • Dead → confirmed. The module cannot supply a sleeping device.

Mark this 🔶 until run on the actual unit — the behaviour is characteristic of the class, but thresholds vary between chips and clones.

Salvage — it is still a perfectly good charger

Ignore the boost output entirely and tap B+ / B− at the cell. You get Type-C charging, cell protection and the fuel-gauge LEDs, and you feed the ESP32’s own LDO directly from the 3.7 V cell. The boost half simply goes unused.

Correct part for a sleeping device: TP4056 + DW01A protection module — ~5 µA standby, about $0.30. Charge only, no boost, nothing that decides to turn itself off.

LDO quiescent current can exceed the MCU’s sleep draw

Pick the regulator on Iq, not on output current

The AP2112K-3.3 — already in the 12 V automotive protection chain in 2026-07-29-hardcut-design (LM74700-Q1 → SMBJ33A → LM5164-Q1 → AP2112K-3.3) — has ~55 µA quiescent ✅. That is five times a sleeping ESP32-S3. Entirely fine on an alternator; fatal on a cell.

PartIqMax outVerdict for battery ESP32
AP2112K-3.3~55 µA600 mA❌ Iq dominates the budget. Fine for hardcut’s 12 V chain, wrong here
MCP1700~1.6 µA250 mAcannot supply WiFi peaks — it will brown out on TX
XC6220~9 µA1 A✅ the one that satisfies both constraints

Two hard constraints that must be met simultaneously, which is what eliminates most of the catalogue:

  1. Iq in single-digit µA, or the regulator becomes the load.
  2. Must still deliver ESP32 WiFi peaks of ~350–500 mA. This is why the very-low-Iq micropower parts do not qualify — see also the 2.43 V brownout threshold in Never power a servo from the board’s 5 V pin.

Do not run an ESP32 directly off a Li-ion cell

A charged cell is 4.2 V. The ESP32 is out of spec above 3.6 V. “It’s roughly 3.7 V, close enough” is only true at mid-discharge — it is over the limit for the whole top of the curve. The LDO is not optional.

Li-ion self-discharge is the floor

Below ~10 µA of device draw, further chip-level optimisation is unmeasurable

Li-ion self-discharge is ~2 %/month ✅. On a 2 800 mAh 18650 that is ~56 mAh/month = ~1.9 mAh/day, versus ~0.24 mAh/day for a sleeping bare S3. The cell leaks about 8× faster than the MCU drinks.

Direct consequence for part selection:

Deep sleep18650 enduranceDifference
ESP32-S3 (bare module)~10 µAself-discharge limited, ~4 y
nRF52840~1 µAself-discharge limited, ~4 ynone you can measure

On an 18650, an nRF52840 buys nothing over an ESP32-S3. The 10× current advantage disappears entirely underneath self-discharge.

Where the nRF advantage is real: cell format, not current

The nRF can run from a coin cell. An ESP32 physically cannot:

  • CR2032 internal resistance is ~10–15 Ω, and it collapses above ~20 mA.
  • ESP32 WiFi peaks at ~350–500 mA. That is more than an order of magnitude past what the cell can source; the rail sags straight through the 2.43 V brownout threshold.

The only way around it is a supercap across the rail to supply the TX bursts — which is a real technique, and also an admission that you picked the wrong chip for the form factor.

So the question is never “which chip sleeps lower.” It is “what cell does the enclosure allow?” 18650/LiPo → ESP32 is fine. Coin cell → it is not an ESP32 project.

A normally-closed switch bleeds current continuously

NC means "a complete circuit 24/7" — including through your bias resistor

A normally-open button costs nothing at rest; it is an open circuit. A normally-closed contact is closed at rest, so whatever resistor biases it is conducting all the time.

On the ESP32’s internal pull-up (~45 kΩ): 3.3 V / 45 kΩ = 73 µA ✅ — seven times the entire MCU sleep budget, from one switch, forever.

The fix — invert it and use a 1 MΩ external pull-down

Wire the NC contact between GPIO and 3V3, with a 1 MΩ pull-DOWN to ground, internal pulls disabled.

  • 3.3 V / 1 MΩ = 3.3 µAbelow the MCU’s own sleep floor. The switch stops mattering.
  • It also inverts the sense: at rest the pin is HIGH (contact closed to 3V3); pressing opens the contact and the 1 MΩ pulls the pin LOW. Same polarity as a normally-open button — LOW = pressed.

That polarity match is not cosmetic:

esp_sleep_enable_ext1_wakeup() takes ONE trigger level for the entire pin mask

Not per-pin. You cannot put a wake-on-LOW button and a wake-on-HIGH button in the same ext1 config. Mixing NO and NC buttons naively means one of them cannot be a wake source at all.

Options: make every button present the same polarity (the 1 MΩ inversion above does exactly this), or move to the per-IO API added in ESP-IDF 5.x (esp_sleep_enable_ext1_wakeup_io()), which does allow per-pin levels. The former is one resistor; the latter is an IDF version floor. Relevant if the firmware is ESP-IDF like 2026-08-11-ruview-esp32-csi-real-hardware rather than Arduino-core 2.0.17 (Arduino core is 2.0.17, not 3.x).

The one thing NC is genuinely better at: it fails loud

FailureNO buttonNC button
Broken wiresilent — reads as “never pressed”, indistinguishable from a working idle buttonloud — reads as permanently pressed

A silent failure on a device that lives in a drawer is the worst possible failure. NC turns it into an obvious one — provided firmware handles the stuck state, which is the next hazard.

SPDT snap-action switches give you both contacts on one part — the SS-5GL already on the bench (hardcut-bench-inventory) has NO and NC terminals, so the choice is a wiring decision, not a purchasing one.

ext1 ANY_LOW wake loop

Entering deep sleep while a masked pin is ALREADY low wakes the device immediately — forever

ESP_EXT1_WAKEUP_ANY_LOW is level-triggered, not edge-triggered. If any pin in the mask is low at the moment esp_deep_sleep_start() is called, the wake condition is already true. The chip sleeps and wakes back-to-back at full boot current until the cell is flat — hours, not days.

A stuck button or a broken NC wire triggers exactly this, which is what makes it the natural sequel to the previous entry: NC’s “fails loud” property presents as a permanently-pressed pin, and a permanently-pressed pin is a permanent wake condition.

Firmware must, before every sleep:

  1. Read the state of every pin in the wake mask.
  2. If a pin is already in the trigger state, mask it out of the wake set.
  3. Sleep on the remaining pins plus a periodic timer so the masked pin gets re-checked and can rejoin once it recovers.
  4. Ideally record the exclusion so the device can report “button 2 stuck” on next contact, rather than silently running with a dead button.

This is the archetypal "runs fine on the bench, dead in a drawer three weeks later" bug

On a bench you press a button, it wakes, you release it, it sleeps — the stuck case never happens because a human is holding it correctly. The failure needs a mechanical fault and time, so it will not appear during development.

Octal PSRAM deep-sleep adder — MEASURE

⚠️ MEASURE — do not budget the ~7 µA datasheet figure for N16R8 parts

Octal-PSRAM parts may add tens of µA in deep sleep, depending on how VDD_SPI is handled. The 7–10 µA figure is for the module generally; it is not confirmed for the R8 (8 MB octal PSRAM) variant specifically.

This matters directly: the 10 bare modules on hand are ESP32-S3-WROOM-1-N16R8-M — the R8 part.

If the adder is real, tens of µA sits in the same range as the Li-ion self-discharge is the floor equivalent (~78 µA) and would meaningfully cut endurance. It would also make an N16R0/N8R0 (no-PSRAM) variant the correct part for battery work — a purchasing decision, not a firmware one.

Resolve with a meter before any endurance claim is made. Until then, treat every 18650 lifetime on this page as an upper bound.

WiFi wake-to-delivered latency is mostly avoidable

Awake time is the energy budget for a duty-cycled device — at ~100 mA average while connecting, the difference between 2.6 s and 0.5 s is ~5× the energy per event. ✅

StageNaiveTunedHow
Boot300 ms300 msirreducible
AP scan~1000 ms~0cache BSSID + channel in RTC slow memory and connect directed
Associate200 ms~150 ms
DHCP~500 ms0static lease / static IP
DNS50 ms0POST to a literal IP
TLS handshake~500 ms0drop TLS on a LAN-only path
POST50 ms50 ms
Total~2.6 s~500 ms

RTC slow memory survives deep sleep — NVS is the wrong place for this

Cache the BSSID and channel in RTC slow memory (RTC_DATA_ATTR), not NVS. It persists across deep sleep, costs nothing, and — critically — avoids a flash write. See Flash operations are the real threat: a flash write disables cache and stalls task code for milliseconds. Writing NVS on every wake would be both slower and pointlessly wearing.

Keep a full-scan fallback — the cached BSSID will miss

This vault has the receipts: 2026-08-26-router-reflash-mac-5ghz-dfs-and-bambu-offline (router reflash, DFS channel change made the 5 GHz SSID invisible) and 2026-08-15-router-wifi-tuning-htmode-band-penetration (deliberate channel/htmode churn). A device that only ever tries its cached BSSID goes permanently offline after a router reboot — and being battery powered, nobody notices until it is dead.

Shape: try cached BSSID+channel → on failure, full scan → write the new BSSID back to RTC memory → proceed. The slow path costs 2 s once; the fast path costs 500 ms forever after.

Also worth noting: 2.4 GHz only. DFS does not apply, but the same reflash/channel churn does.

Part Selection Summary

Function✅ Use❌ AvoidWhy
MCU carrierBare WROOM-1 moduleAny DevKitCH343 + AMS1117 + LED = 20–45 mA ⚠️ verify PSRAM adder
Charger / protectionTP4056 + DW01A (~5 µA, ~$0.30)MH-CD42, IP5306, any power bankauto-shut-off below ~40–50 mA
RegulatorXC6220 (9 µA, 1 A)AP2112K (55 µA), MCP1700 (250 mA)Iq and 500 mA peak, both required
IndicatorPlain LED on GPIOWS2812 without a FET~1 mA showing black
ButtonNC + 1 MΩ pull-down to GND, internal pulls offNC on internal pull-up3.3 µA vs 73 µA; also fixes ext1 polarity
Cell18650 / LiPoCR203210–15 Ω ESR collapses at ~20 mA; WiFi wants 350 mA
Wake on buttonESP32 (ext0/ext1)ESP8266ESP8266 has no GPIO deep-sleep wake at all