How to Achieve 2-Year Battery Life with ESP32

Your ESP32 prototype works beautifully on your bench. It reads sensors, connects to WiFi, maybe even scans for Bluetooth devices. Then you plug in a battery and watch it die in three days.
Here’s the uncomfortable truth: that ESP32 sitting idle in your project box is burning through 30-50mA just existing. At that rate, a beefy 3000mAh battery lasts about four days. Yet the same chip can run for two years on the same battery if you understand the single technique that makes it possible.
The datasheet claims 10µA deep sleep current. Real-world boards hit 10-150µA depending on your configuration. The gap between “datasheet dreams” and “deployed reality” is where most hobbyist projects fail. This guide bridges that gap with practical firmware patterns, honest power math, and battery choices that actually work for multi-year deployments.
Why Deep Sleep Isn’t Optional
The ESP32 has four power states that matter: Active, Modem Sleep, Light Sleep, and Deep Sleep. For multi-year battery life, only one of these matters.
Active mode runs the full processor at 80-240MHz, drawing 100-240mA depending on what peripherals you’re using. WiFi transmission spikes even higher. This is where your battery goes to die.
Modem Sleep and Light Sleep reduce power by turning off radios or pausing the CPU, but they still draw 1-20mA. Better, but nowhere near good enough.
Deep Sleep shuts down almost everything. The main CPU stops. WiFi and Bluetooth power off. Only the RTC (Real-Time Clock) controller and RTC memory stay alive, maintaining just enough awareness to wake up when something interesting happens. Current draw: 10-150µA depending on your board and configuration.
That’s a 1,000x difference between active and deep sleep. No amount of clever optimization in active mode compensates for skipping deep sleep entirely.
Variant Matters More Than You Think
Not all ESP32s sleep equally:
| Variant | Typical Deep Sleep Current | Best For |
|---|---|---|
| ESP32 (original) | 10-20µA | General purpose, legacy projects |
| ESP32-S2 | 20-25µA | USB-native applications |
| ESP32-S3 | 7-10µA | AI/ML with low power needs |
| ESP32-C3 | 5-10µA | BLE-focused, cost-sensitive |
The ESP32-C3 deserves special attention for battery projects. Its RISC-V core and efficient BLE implementation make it the best choice for beacon and sensor applications where you need occasional wireless communication.
Wake Source Strategy Determines Everything
Deep sleep is useless if your device never wakes up, or wakes up too often. Your wake source strategy defines your entire power profile.
Always-Listening BLE: The Hard Problem
Detecting nearby phones or beacons sounds simple until you do the math. “Always listening” for Bluetooth means the radio must periodically scan, and radios are power-hungry.
The practical approach: duty-cycled scanning windows. Instead of continuous scanning, wake every few seconds, scan briefly, then sleep again.
#include <BLEDevice.h>
#include <esp_sleep.h>
#define SCAN_TIME_SECONDS 1
#define SLEEP_TIME_SECONDS 10
#define TARGET_BEACON_NAME "MyBeacon"
RTC_DATA_ATTR int bootCount = 0;
void setup() {
bootCount++;
BLEDevice::init("");
BLEScan* scanner = BLEDevice::getScan();
scanner->setActiveScan(false); // Passive scan uses less power
scanner->setInterval(100);
scanner->setWindow(100);
BLEScanResults results = scanner->start(SCAN_TIME_SECONDS, false);
bool targetFound = false;
for (int i = 0; i < results.getCount(); i++) {
BLEAdvertisedDevice device = results.getDevice(i);
if (device.getName() == TARGET_BEACON_NAME) {
targetFound = true;
break;
}
}
if (targetFound) {
// Do your thing: trigger relay, send notification, log event
handleBeaconDetected();
}
BLEDevice::deinit(true); // Critical: fully shut down BLE
esp_sleep_enable_timer_wakeup(SLEEP_TIME_SECONDS * 1000000ULL);
esp_deep_sleep_start();
}
void loop() {
// Never reached - deep sleep resets the chip
}
void handleBeaconDetected() {
// Your application logic here
}The power reality of this pattern: scanning for 1 second at ~100mA, then sleeping for 10 seconds at ~10µA, averages to roughly 9mA. That’s still only a few weeks on a typical battery, not years.
To reach multi-year territory with BLE scanning, you need longer sleep intervals (scan every 30-60 seconds) or event-triggered wake-ups that eliminate scanning when nothing’s happening.
Event-Triggered Wake-Ups: The Easier Path
If your project can wait for an external signal rather than continuously polling, power consumption drops dramatically.
Timer wake works for periodic sensor readings:
#define MINUTES_BETWEEN_READINGS 15
void setup() {
// Read sensor, store or transmit data
float temperature = readSensor();
transmitData(temperature);
// Sleep for 15 minutes
esp_sleep_enable_timer_wakeup(MINUTES_BETWEEN_READINGS * 60 * 1000000ULL);
esp_deep_sleep_start();
}GPIO wake responds to external events like PIR motion sensors or button presses:
#define WAKE_PIN GPIO_NUM_33
void setup() {
// Handle the event that woke us
handleMotionDetected();
// Configure wake on rising edge (PIR output goes high on motion)
esp_sleep_enable_ext0_wakeup(WAKE_PIN, 1);
esp_deep_sleep_start();
}Touch wake uses the ESP32’s capacitive touch pins:
void setup() {
// Threshold depends on your hardware - lower = more sensitive
touchSleepWakeUpEnable(T0, 40); // GPIO4
esp_deep_sleep_start();
}You can combine wake sources. A weather station might wake every hour on a timer, but also wake immediately if a button is pressed for manual readings.
Power Budget Math You Can Actually Use
The formula is straightforward:
Average Current = (active_time × active_current + sleep_time × sleep_current) / total_cycle_time
Example 1: Temperature Sensor
Wake every 15 minutes, read sensor and transmit via WiFi for 5 seconds.
- Active: 5 seconds at 150mA (WiFi transmission)
- Sleep: 895 seconds at 15µA
- Cycle: 900 seconds total
Average = (5 × 150 + 895 × 0.015) / 900 = 0.85mA
A 3000mAh battery at 0.85mA average: 3,500 hours = 146 days
That’s not two years, but it’s a reasonable starting point. Reduce transmission frequency to once per hour and you’re at 0.22mA average, pushing past a year.
Example 2: Motion-Triggered Camera Alert
Mostly sleeping, wakes on PIR trigger, sends photo upload.
- Active: 10 seconds at 200mA (camera + WiFi), 5 events per day
- Sleep: remaining time at 15µA
Daily consumption: (50 seconds × 200mA) + (86,350 seconds × 0.015mA) = 10,000mAs + 1,295mAs = 11,295mAs
Daily average: 11,295 / 86,400 = 0.13mA
A 3000mAh battery: 23,000 hours = 2.6 years
Quick Reference
| Average Current | 3000mAh Battery Life |
|---|---|
| 1mA | 125 days |
| 500µA | 250 days |
| 200µA | 1.7 years |
| 100µA | 3.4 years |
| 50µA | 6.8 years |
Target 50-100µA average for realistic two-year operation.
Picking the Right Battery
Your code can be perfect, but the wrong battery chemistry kills multi-year projects before they start.
Lithium Thionyl Chloride (Li-SOCl₂, 3.6V): The correct choice for true multi-year deployments. Self-discharge under 1% per year. Ten-year shelf life. Non-rechargeable, but if you’re targeting 2+ years, you’re not recharging anyway. SAFT LS14500 (AA size, 2600mAh) is the classic choice.
LiFePO4 (3.2V): Low self-discharge around 3% per year. Rechargeable. Safer chemistry than standard lithium. Good option if you want solar recharging capability.
Standard LiPo (3.7V): Loses 5-10% capacity per year just sitting. After two years, you’ve lost 10-20% of your battery to self-discharge alone, before your circuit consumed anything. Not ideal for multi-year, but workable for 6-12 month projects.
AA Lithium (Energizer Ultimate): Excellent cold tolerance, predictable behavior, 3000mAh capacity. Good for outdoor deployments where temperature swings matter.
CR2032: Avoid for anything beyond the lowest-power applications. At 220mAh capacity, even 50µA average gives you only six months.
Recommendation: For a two-year deployment, use a Lithium Thionyl Chloride cell (SAFT LS14500 or equivalent) with a simple LDO regulator. The higher upfront cost pays off when you’re not replacing batteries annually.
The Pre-Sleep Checklist
Before calling esp_deep_sleep_start(), run through this checklist:
- Disable WiFi and Bluetooth explicitly:
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
btStop();- Disable ADC if not needed for wake:
adc_power_off();- Set unused pins to known states (floating pins waste power):
gpio_set_direction(GPIO_NUM_XX, GPIO_MODE_INPUT);
gpio_pulldown_en(GPIO_NUM_XX);- Disable serial output (USB serial keeps peripherals powered):
Serial.end();- Store persistent data in RTC memory:
RTC_DATA_ATTR int persistentCounter = 0; // Survives deep sleep
Testing Without Lab Equipment
A proper current measurement setup costs hundreds of dollars. Here’s how to validate your design without one.
USB power meters ($10-15) show current draw during active periods. They lack resolution for sleep current but confirm your active consumption matches expectations.
The week-long test: Deploy your project with a fully charged battery and a voltage monitoring log. Run for one week, extrapolate. If you’re at 90% battery after a week, you’re losing 10% per week, nowhere near two-year territory. If you’re at 99.5% after a week, you’re on track.
Trust your math, verify your assumptions: The power budget calculation works if your inputs are correct. Measure active time with timestamps. Assume worst-case sleep current (use 50µA if unsure). Build in a 20% safety margin.
Making This Work This Weekend
Two-year ESP32 battery life comes down to four decisions:
Use deep sleep aggressively. Every second in active mode costs 10,000 times more than sleeping.
Choose wake sources that match your actual needs. Timer for periodic sensing, GPIO for event response, duty-cycled BLE only when truly necessary.
Do the power math before building. Average current determines runtime. Work backward from your target to validate feasibility.
Pick a battery chemistry that won’t self-discharge your project into failure. Lithium thionyl chloride for multi-year, LiFePO4 for rechargeable long-term.
Start with your simplest project: a temperature logger with 15-minute readings or a door sensor that wakes on GPIO. Get the sleep/wake cycle working, measure your results for a week, and build confidence before tackling always-listening BLE. The patterns transfer; the math scales.
Your ESP32 can outlast most household appliances. The techniques aren’t exotic. The math isn’t hard. The chips are capable. Now you know how to use them.
Hubble Network connects devices like these directly to satellites—no gateways, no infrastructure, just your sensor and the sky. See how it works →