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.
ext1ANY_LOWentered 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 anN16R8→ 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.
| Mark | Meaning |
|---|---|
| ✅ | 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 |
| ⚠️ MEASURE | Unverified. 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.
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 draw 18650 Bare ESP32-S3-WROOM-1 module ~7–10 µA ~30 years DevKit carrier: CH343 USB-UART + AMS1117 LDO + power LED 20–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:
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/ext1equivalent in ESP8266EX. Not "awkward" — absentThe 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 µA — worse 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
VDDis 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:
- Plain LEDs on GPIO. A GPIO driven low sinks nothing. If the UI is one or two indicators, the addressable pixel buys nothing.
- Gate
VDDthrough 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.
| Part | Iq | Max out | Verdict for battery ESP32 |
|---|---|---|---|
| AP2112K-3.3 | ~55 µA | 600 mA | ❌ Iq dominates the budget. Fine for hardcut’s 12 V chain, wrong here |
| MCP1700 | ~1.6 µA | 250 mA | ❌ cannot supply WiFi peaks — it will brown out on TX |
| XC6220 | ~9 µA | 1 A | ✅ the one that satisfies both constraints |
Two hard constraints that must be met simultaneously, which is what eliminates most of the catalogue:
- Iq in single-digit µA, or the regulator becomes the load.
- 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 sleep | 18650 endurance | Difference | |
|---|---|---|---|
| ESP32-S3 (bare module) | ~10 µA | self-discharge limited, ~4 y | — |
| nRF52840 | ~1 µA | self-discharge limited, ~4 y | none 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 µA — below 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 maskNot per-pin. You cannot put a wake-on-LOW button and a wake-on-HIGH button in the same
ext1config. 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
Failure NO button NC button Broken wire silent — reads as “never pressed”, indistinguishable from a working idle button loud — 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_LOWis level-triggered, not edge-triggered. If any pin in the mask is low at the momentesp_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:
- Read the state of every pin in the wake mask.
- If a pin is already in the trigger state, mask it out of the wake set.
- Sleep on the remaining pins plus a periodic timer so the masked pin gets re-checked and can rejoin once it recovers.
- 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
N16R8partsOctal-PSRAM parts may add tens of µA in deep sleep, depending on how
VDD_SPIis 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. ✅
| Stage | Naive | Tuned | How |
|---|---|---|---|
| Boot | 300 ms | 300 ms | irreducible |
| AP scan | ~1000 ms | ~0 | cache BSSID + channel in RTC slow memory and connect directed |
| Associate | 200 ms | ~150 ms | — |
| DHCP | ~500 ms | 0 | static lease / static IP |
| DNS | 50 ms | 0 | POST to a literal IP |
| TLS handshake | ~500 ms | 0 | drop TLS on a LAN-only path |
| POST | 50 ms | 50 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 | ❌ Avoid | Why |
|---|---|---|---|
| MCU carrier | Bare WROOM-1 module | Any DevKit | CH343 + AMS1117 + LED = 20–45 mA ⚠️ verify PSRAM adder |
| Charger / protection | TP4056 + DW01A (~5 µA, ~$0.30) | MH-CD42, IP5306, any power bank | auto-shut-off below ~40–50 mA |
| Regulator | XC6220 (9 µA, 1 A) | AP2112K (55 µA), MCP1700 (250 mA) | Iq and 500 mA peak, both required |
| Indicator | Plain LED on GPIO | WS2812 without a FET | ~1 mA showing black |
| Button | NC + 1 MΩ pull-down to GND, internal pulls off | NC on internal pull-up | 3.3 µA vs 73 µA; also fixes ext1 polarity |
| Cell | 18650 / LiPo | CR2032 | 10–15 Ω ESR collapses at ~20 mA; WiFi wants 350 mA |
| Wake on button | ESP32 (ext0/ext1) | ESP8266 | ESP8266 has no GPIO deep-sleep wake at all |
Related
- esp32 — the parent reference; hazards 1–11, pins, flashing, toolchain, RTOS
- Boards on Hand — the stock this section reclassifies: bare modules ✅, DevKits ❌
- 2026-07-29-hardcut-design — the
LM74700-Q1 → SMBJ33A → LM5164-Q1 → AP2112K-3.3chain the 55 µA LDO comes from, and the 74HC74 interlock precedent - hardcut-bench-inventory — 74HC74 dual D flip-flops and SS-5GL SPDT (NO+NC) microswitches already in hand
- lightaim — single-button UX and resistive-divider handling on ADC1
- 2026-08-11-ruview-esp32-csi-real-hardware — the ESP-IDF/NVS-provisioned project, relevant to the IDF-5.x per-IO
ext1API - 2026-08-26-router-reflash-mac-5ghz-dfs-and-bambu-offline — why a cached BSSID needs a full-scan fallback
- 2026-08-15-router-wifi-tuning-htmode-band-penetration — ongoing channel/htmode churn on the same network
- LOG
- TOPICS