ESP32 Sleep Modes Explained: Deep Sleep vs Light Sleep for Maximum Battery Life
If you've ever left an ESP32 running on batteries and come back a day later to find them dead, you already know the problem this article solves. An ESP32 pulling Wi-Fi at full power can draw well over 150–240 mA. A pair of AA batteries or a small LiPo cell simply can't keep up with that for long. The fix isn't a bigger battery — it's teaching your board to sleep properly.
This guide breaks down every sleep mode the ESP32 supports, shows you working Arduino code for each one, and helps you pick the right wake-up source for your project — whether that's a soil moisture sensor checking in every hour or a doorbell that needs to react instantly.
If you haven't already gotten your board talking to your network, it's worth setting that up first before you start layering sleep logic on top of it.
Why Power Modes Matter More Than You Think
The ESP32 was never designed to run flat-out all the time. In active mode — CPU running, radio on — the chip and a typical dev board around it can pull anywhere from 75 mA at idle up to 240 mA during a Wi-Fi transmission burst. Do the math on a 2000 mAh battery and you're looking at well under a day of runtime.
Drop that same board into deep sleep, and current draw falls to somewhere between 10 µA and 150 µA depending on which wake-up sources you leave active. That's not a small improvement — it's roughly a 1,000x to 10,000x reduction. A project that died in 8 hours can realistically run for 6–12 months on the same battery, provided it spends most of its life asleep and only wakes briefly to do actual work.
The Three Power States You Need to Know
| Mode | Typical Current | What Stays On | RAM Retained? | Wake Speed |
|---|---|---|---|---|
| Active | 75–240 mA | Everything | Yes | — |
| Light Sleep | ~0.8 mA | CPU clock paused, RAM and peripherals held in state | Yes — code resumes where it left off | Milliseconds |
| Deep Sleep | 10 µA – 150 µA | Only RTC controller, RTC memory, and optionally the ULP co-processor | No — main RAM is wiped | Behaves like a reset |
| Hibernation | ~5 µA | Only a single RTC timer on the slow clock | No | Slowest |
The trade-off is simple: the deeper the sleep, the less power you use, but the more of the chip's state you lose. Light sleep keeps your program's variables and picks up exactly where it paused. Deep sleep is closer to pulling the plug and plugging it back in — your setup() function runs again from scratch, and anything you want to survive the sleep cycle has to be explicitly saved.
Light Sleep: The Middle Ground
Light sleep pauses the CPU clock and most digital peripherals but keeps your RAM powered, so execution resumes exactly where it left off — no reset, no re-running setup(). It's the right choice when you need moderate power savings but also need the board to react quickly, like a sensor that briefly pauses between polling cycles but can't afford the reset penalty of deep sleep.
Putting the ESP32 into light sleep only takes one function call once your wake-up source is configured:
#include "esp_sleep.h"
#define WAKE_PIN GPIO_NUM_33
void setup() {
Serial.begin(115200);
delay(1000);
// Wake up when this pin goes HIGH
esp_sleep_enable_ext0_wakeup(WAKE_PIN, 1);
Serial.println("Going into light sleep...");
Serial.flush();
esp_light_sleep_start();
Serial.println("Woke up from light sleep!");
}
void loop() {
// Execution continues normally after waking
Serial.println("Running task...");
delay(2000);
}Light sleep also supports GPIO wake-up and UART wake-up, which deep sleep does not — handy if you want the board to wake the moment data arrives on the serial line.
Deep Sleep: Maximum Savings, With a Catch
Deep sleep is where most battery-powered ESP32 projects live. The CPU and Wi-Fi radio shut down completely; only the RTC controller and a small pocket of RTC memory stay powered. Because main RAM is cut off, waking up behaves like a fresh boot — your code restarts from the top of setup().
If you need a value to survive the sleep cycle (like a boot counter, a sensor reading, or a Wi-Fi reconnection flag), store it in RTC memory using the RTC_DATA_ATTR attribute:
#include "esp_sleep.h"
#define uS_TO_S_FACTOR 1000000ULL
#define SLEEP_SECONDS 300 // sleep for 5 minutes
RTC_DATA_ATTR int bootCount = 0;
void setup() {
Serial.begin(115200);
delay(500);
bootCount++;
Serial.printf("Boot number: %d\n", bootCount);
esp_sleep_wakeup_cause_t wakeupReason = esp_sleep_get_wakeup_cause();
if (wakeupReason == ESP_SLEEP_WAKEUP_TIMER) {
Serial.println("Woken up by timer");
}
// --- Do your actual work here: read a sensor, send data, etc. ---
esp_sleep_enable_timer_wakeup(SLEEP_SECONDS * uS_TO_S_FACTOR);
Serial.println("Going to deep sleep now...");
Serial.flush();
esp_deep_sleep_start();
}
void loop() {
// Never reached — the board resets on wake
}Choosing a Wake-Up Source
Deep sleep supports several wake-up triggers, and you can combine more than one:
- Timer — wakes the board after a set number of microseconds. Best for periodic sensor readings (temperature loggers, soil moisture checks, battery-powered weather stations).
- External wake-up, ext0 — wakes on a single RTC-capable GPIO pin reaching a defined level. Great for a push button, PIR motion sensor, or magnetic reed switch.
- External wake-up, ext1 — like ext0, but can watch several RTC GPIO pins at once using a bitmask, useful for a small keypad or multiple trigger points.
- Touch pins — wakes when a capacitive touch pin is triggered, no mechanical button required.
- ULP co-processor — a tiny ultra-low-power core that can keep reading a sensor on its own and only wake the main CPU when a threshold is crossed. This is the most advanced option and the most power-efficient for sensor-driven wake-ups.
A minimal example using a button on an RTC GPIO pin:
#define WAKEUP_PIN GPIO_NUM_33
void setup() {
Serial.begin(115200);
esp_sleep_wakeup_cause_t reason = esp_sleep_get_wakeup_cause();
if (reason == ESP_SLEEP_WAKEUP_EXT0) {
Serial.println("Woke up because of button press!");
}
esp_sleep_enable_ext0_wakeup(WAKEUP_PIN, 1); // wake on HIGH
esp_deep_sleep_start();
}
void loop() {}Real-World Battery Life Example
Say you're building a soil moisture sensor that wakes every 10 minutes, takes a reading, sends it over Wi-Fi, and goes back to sleep. Each active cycle — including the Wi-Fi handshake and data send — takes roughly 3 seconds at an average of 150 mA. The other 597 seconds are spent in deep sleep at around 15 µA.
Rough daily consumption:
- Active time: 3 seconds × 144 cycles/day × 150 mA ≈ 18 mAh
- Sleep time: ~23.9 hours × 0.015 mA ≈ 0.36 mAh
That's under 20 mAh per day. A 2000 mAh battery could realistically run this setup for over three months, even accounting for real-world inefficiencies like voltage regulator losses and self-discharge. This is the exact calculation you should run for your own project before picking a battery — swap in your own active-time and sleep-current numbers.
Common Mistakes That Kill Battery Life
- Forgetting to disconnect Wi-Fi before sleeping. Call
WiFi.disconnect(true)andWiFi.mode(WIFI_OFF)before entering deep sleep if you enabled Wi-Fi during the active cycle. - Leaving unused wake-up sources enabled. Every additional RTC GPIO or the ULP co-processor left running adds a few extra µA. Only enable what you actually need.
- Using a dev board for final deployment. Many ESP32 dev boards have an onboard USB-to-serial chip and power LED that stay on and quietly draw several extra mA, even in deep sleep. For a real deployed project, use a bare module or a board specifically designed for low power, and physically remove or desolder the power LED if needed.
- Not accounting for voltage regulator quiescent current. A cheap AMS1117 regulator can burn several mA on its own, which can dwarf your carefully optimized 15 µA sleep current. Look for a low-dropout regulator with low quiescent draw for battery projects.
- Re-initializing Wi-Fi from scratch every wake cycle. Reconnecting to Wi-Fi from zero can take several seconds of high current draw. Storing the channel and BSSID in RTC memory and using a fast-reconnect approach can cut that time significantly — something worth combining with your OTA update routine so firmware stays current without waking the board more than necessary.
Frequently Asked Questions
What's the difference between ESP32 deep sleep and light sleep?
Light sleep keeps RAM powered and resumes code exactly where it paused, using around 0.8 mA. Deep sleep cuts power to almost everything except the RTC, uses as little as 10 µA, but restarts your program from setup() on wake.
Does the ESP32 lose Wi-Fi credentials after deep sleep?
No. Credentials stored in flash (via WiFi.begin() with saved settings) persist. What's lost is the active connection state and anything held in regular RAM that wasn't stored in RTC memory.
How long can an ESP32 run on a battery in deep sleep? It depends entirely on how often you wake up and what you do during each active cycle. A board waking briefly every few minutes to send a small reading can often run for several months to over a year on a single 18650 or LiPo cell.
Can I use deep sleep with the ULP co-processor to save even more power? Yes. The ULP co-processor can keep monitoring a sensor while the main CPU stays asleep, only waking the CPU when a condition is met. This is the most power-efficient pattern for continuous sensor monitoring.
If you're working with ESP32 for a battery-powered or off-grid project, getting sleep modes right is usually the single biggest lever you have over runtime. Start with the timer wake-up example above, measure your actual current draw with a multimeter or a tool like a Nordic Power Profiler, and tune from there.
Want to turn projects like this into income? Building and documenting hardware projects is a skill — and there are practical ways to turn that skill into extra income on the side. If you're curious about legitimate ways to start earning online alongside your maker projects, check out petlifehacks.page for more ideas.
