Your ESP32-C6 BLE Beacon Wakes Up Fine — So Why Is It Still Draining the Coin Cell in Weeks?

Diagnosing hidden power drains that kill coin cell life in ESP32-C6 BLE beacons

Your firmware works. The device enters deep sleep, wakes on the RTC timer, fires an advertisement, goes back down. You scoped it. The logic is clean.

And the CR2032 is dead in three weeks.

“Enters sleep” and “sleep floor is actually low” are different claims, and only one is true in your build. A beacon can sleep perfectly and still burn 200 µA on the flat segment because something never powered down. The radio isn’t your problem. The 99% of the time it’s not transmitting is your problem.

Generic ESP32 low-power tutorials get you to “call esp_deep_sleep_start()” and stop. They don’t tell you what a leaking floor looks like on a power profiler, and they don’t do the coin-cell math. Let’s do both.

Step 1: Establish the Budget First With Coin-Cell Math

A CR2032 has a hard ceiling. Nominal capacity is around 225 mAh, but you don’t get all of it. Under pulsed BLE loads and real temperatures, usable capacity for a coin cell BLE design lands closer to 200-220 mAh once you derate for the voltage cliff and pulse behavior. Use 210 mAh as a working number and adjust for your cell’s datasheet.

The governing math:

Lifetime (years) ≈ Usable_mAh / (Avg_current_µA × 8.76)

Avg current = I_sleep + (Q_adv_event / Adv_interval)

Target sleep floor:  < 10 µA  → multi-year territory
Reality if leaking:  ~200 µA  → weeks

That 8.76 is hours-per-year divided by 1000 (µA to mA). The whole game is average current, and average current is two terms: the sleep floor you sit at constantly, and the advertising energy amortized across the interval.

Worked example at 210 mAh:

| Scenario           | Avg current | Lifetime      |
|--------------------|-------------|---------------|
| Clean floor        | 8 µA        | ~3.0 years    |
| One small leak     | 50 µA       | ~0.48 years   |
| Floor + hot GPIO   | 200 µA      | ~0.12 years   |

That last row is your three weeks. The leak, not the radio, killed it. If the number you want isn’t achievable even with a perfect floor, no firmware trick saves you, and you learn that now instead of after two weeks of profiling.

Step 2: Measure the True ESP32 Deep Sleep Current

Config lies. Measure.

Put a PPK2 or Joulescope inline and capture a full cycle. You want two things from the trace: the flat sleep segment between advertisements, and the advertising bursts themselves. Run the profiler in DC mode to read the steady floor, then switch to a higher sample rate to catch the burst shape and integrate charge per event.

For the ESP32-C6, anchor on the RTC-timer-only deep sleep case, because that’s the beacon: no touch, no external wake pins holding a domain up, just the RTC timer counting. Espressif quotes deep sleep in the single-digit µA range for the C6 with RTC timer wake (roughly 7 µA). Verify that against the current datasheet revision for your package. Light sleep with RTC peripherals active runs far higher, hundreds of µA, so if you accidentally landed in light sleep you’ll see it immediately.

Here’s the discrepancy signature. If your flat floor reads 7-10 µA, your sleep path is clean and you skip to Step 5. If it reads tens or hundreds of µA above spec, you have a leak, and no amount of advertising tuning will fix a floor that’s 20× too high. Nail the floor first.

Step 3: Hunt the Peripheral and GPIO Leaks

This is where the µA hide. The usual suspects, ranked by how often they’re the actual culprit:

| Leak source          | Typical cost | Fix                        |
|----------------------|--------------|----------------------------|
| Floating GPIO        | 10–100 µA    | gpio_hold_en / config      |
| UART left enabled    | ~1 mA        | disable before sleep       |
| Dev-board LED/reg    | 50–500 µA    | measure bare module        |
| Pull-up + sleep leak | 5–50 µA      | high-value R / disable     |

A floating input sitting mid-rail lets the input buffer draw crossbar current continuously. On the C6 you fix this by driving the pin to a rail before sleep and latching it with gpio_hold_en(), or by configuring it as a defined input with a pull. Every unused pin needs a defined state.

Peripherals are the next tier. A UART left clocked, an ADC left biased, an I2C pull-up sourcing current into a sleeping sensor: all of it adds up. Disable peripherals explicitly before you sleep instead of assuming deep sleep gates them. External flash and PSRAM need their own low-power commands; they don’t power down for free.

The trap that burns the most hours: profiling on a dev board. The onboard USB-UART bridge, the power LED, and a lossy 3.3V regulator can add 50-500 µA that has nothing to do with your firmware. If your numbers look insane, measure the bare module.

Isolate by bisection. Disable a subsystem, re-measure the floor, note the delta. Sensor init off drops you 40 µA? Found one. Keep cutting until the floor hits spec. It’s tedious and it’s the only method that works.

Step 4: The C6-Specific Gotchas

Here’s what trips up engineers porting firmware from an S3 or C3. The C6 isn’t a drop-in.

Power domains and wake sources. Some wake sources keep a power domain alive across deep sleep. If you configure a GPIO wake or keep an RTC peripheral running, you pay for the domain that feeds it. For a pure RTC-timer beacon, make sure you’re not accidentally requesting ESP_PD_OPTION_ON for domains you don’t need. Check the technical reference manual for which peripheral sits in which domain on the C6 specifically, because the mapping differs from earlier parts.

GPIO hold. The C6 needs explicit hold to keep pin states latched through deep sleep, and you have to release the hold after wake or the pin stays frozen and confuses your next cycle. Miss the release and you’ll chase a phantom.

Brownout detector. The BOD adds quiescent current while it monitors the rail. Near a coin cell’s end-of-life voltage that’s a real tradeoff: leave it on and pay the draw, or disable it and risk a brownout-induced flash corruption as the cell sags. For most beacons, keep it on and accept the microamps; the reliability is worth it.

The 802.15.4 radio. The C6 ships with Thread/Zigbee hardware alongside BLE. If your build pulls in coexistence or leaves that radio domain powered, you’re feeding silicon you never use. Confirm only the BLE stack is initialized and the 802.15.4 domain stays down.

Step 5: Tune the Advertising Interval for Battery

Floor’s clean. Now the active side, where the advertising interval tradeoff dominates.

Model one concrete event so you can re-run the math for your own payload. Take an ADV_NONCONN_IND on all 3 primary channels, a small state payload of roughly 20 bytes, TX power at 0 dBm. The wake, radio ramp, 3-channel transmit, and settle cost on the order of Q_adv_event ≈ 12 µC per burst on the C6. Measure yours by integrating the burst in your profiler. This number moves with payload size and TX power.

Amortize that charge across the interval and add the 8 µA floor:

| Adv interval | Amortized adv | Avg current | Est. CR2032 life | Discoverability   |
|--------------|---------------|-------------|------------------|-------------------|
| 100 ms       | 120 µA        | ~128 µA     | ~0.19 yr (weeks) | excellent         |
| 1 s          | 12 µA         | ~20 µA      | ~1.2 yr          | good              |
| 10 s         | 1.2 µA        | ~9.2 µA     | ~2.6 yr          | poor (needs net)  |

Same firmware, same clean floor, 14× the lifetime just by stretching the interval. The active term collapses fast because it’s Q / interval.

The cost is discoverability. A phone doing occasional passive scans catches a 100 ms beacon constantly and a 10 s beacon almost never. That’s why most guides tell you to keep intervals tight, and that’s the constraint they never question.

It only holds if your receiver is intermittent. A BLE observer network that continuously ingests advertisements, like the Hubble network, reliably picks up even multi-second-interval beacons because something is always listening. Offload discoverability to the network and you can push the interval way out, land in multi-year territory, and still locate the device. See the asset tracking implementation guide for how that ingestion model shapes interval choices.

Step 6: Verify by Re-Measuring the Full Cycle

Don’t trust the fix until the waveform proves it.

Capture a complete sleep-plus-advertise period on the profiler and integrate to a real average current. Spot readings lie here: the floor looks great, then a burst you forgot to count drags the average up. Integrate across at least one full advertising interval, ideally several, so every burst and every settle transient is inside the window.

Take that integrated average, drop it back into the Step 1 equation, and confirm you hit target. If you modeled 9.2 µA at a 10 s interval and measure 9 µA, you’re done. If you measure 30 µA, there’s a burst or a residual leak you haven’t accounted for. Go back to bisection.

From Weeks to Years

Two levers move ESP32-C6 battery life, and both need instrumentation, not config faith. The sleep floor is a hunt: measure it, bisect the leaks, get it to single-digit µA. The advertising interval is a dial: every second you add to it roughly divides the active energy budget.

The interval dial only opens up when discoverability isn’t your problem to solve on the device. Hand that to a network that’s always listening, stretch the interval to multi-second, and the same coin cell that died in three weeks runs for years.


Writer’s note on figures: the ~7 µA C6 deep sleep floor, ~12 µC per advertising event, and derived lifetime figures are modeling estimates and must be verified against the current Espressif ESP32-C6 datasheet revision and measured on the target hardware before publishing. The Q_adv_event value in particular is package- and payload-dependent and should be re-measured, not quoted as authoritative.


Hubble Network handles the discoverability side, so your beacon can advertise at multi-second intervals and still be heard — turning the interval dial into years of battery life. See how it works →