In this guide
A battery-powered ESP32 device lives or dies by a single calculation: your average current draw, worked out from how long the chip is awake versus asleep. Get the duty cycle right and a single 18650 cell can run a field sensor for months; get it wrong — a cheap dev board left with WiFi on — and the same cell is dead before dinner.
This guide walks through the complete low-power design of an ESP32 field device: the battery-life equation with a worked comparison table, why most dev boards waste milliamps in "sleep", how deep sleep and wake sources actually work, battery and charger choices, radio selection, and solar top-up sizing. Every number below is either an Espressif datasheet figure or a design target you can verify yourself with a multimeter — nothing here requires a lab bench to check.
The one equation that decides everything
Battery life (hours) = usable battery capacity (mAh) / average current (mA)
Average current is what you control:
Average current = (awake current × awake time + sleep current × sleep time) / total cycle time
A typical field sensor wakes up, reads a sensor, sends the data, and goes back to sleep. Say the wake cycle takes 20 seconds at roughly 180 mA (WiFi transmitting, sensor powered), and the device sleeps at 15 µA (0.015 mA) for the rest of the cycle. Two reporting intervals compared, using one 18650 cell at a realistic usable capacity of 2400 mAh (a 3000 mAh cell derated 20% for aging, temperature, and self-discharge):
| Parameter | Report every 10 min | Report every 60 min |
|---|---|---|
| Awake: 20 s at 180 mA | 20 / 600 s duty | 20 / 3600 s duty |
| Sleep: 15 µA for the rest | 580 / 600 s duty | 3580 / 3600 s duty |
| Average current | (180 × 20 + 0.015 × 580) / 600 ≈ 6.0 mA | (180 × 20 + 0.015 × 3580) / 3600 ≈ 1.01 mA |
| Battery life from 2400 mAh | 2400 / 6.0 ≈ 400 h ≈ 16 days | 2400 / 1.01 ≈ 2376 h ≈ 99 days |
Three observations that should reshape how you design:
- The interval dominates everything. Going from 10-minute to 60-minute reports gives roughly 6× the battery life. Before touching hardware, ask whether the application really needs frequent updates. A soil-moisture irrigation node reporting hourly is a ~3-month device; reporting every 10 minutes is a ~2-week device on the same cell.
- Sleep current matters more than you think. With a 10 µA sleep the 60-minute column stretches past 100 days. With a 5 mA sleep (a bad dev board, discussed below), the 60-minute column collapses to about 20 days — the interval barely matters anymore because the sleep floor eats the battery.
- Awake time is the other lever. Twenty seconds awake per cycle assumes a slow WiFi connection. Cut that to 5 seconds (static IP, fast sensor read, short payload) and the 60-minute column gains another ~25%.
Design rule of thumb: decide the sleep current first, then the report interval, then optimise the awake window. In that order.
Why your prototype dies in six hours: the dev-board drain problem
Here is the failure almost every student hits: the code puts the ESP32 into deep sleep, the firmware is correct, and the battery still dies overnight. The culprit is the board, not the code.
The classic low-cost ESP32 dev board (the common 30-pin "DevKit" style) carries an AMS1117-3.3 linear regulator. The AMS1117 datasheet gives a quiescent current of about 5 mA typical — current the regulator burns just to exist, even when the ESP32 itself is in deep sleep at 10 µA. Add the USB-to-UART bridge chip (usually held out of full suspend on cheap boards), the power LED, and a few pull-ups, and a "sleeping" dev board typically draws 5–15 mA from the battery. At 10 mA, a 3000 mAh cell lasts 300 hours — under two weeks — before you have sent a single packet.
This is why a naive WiFi project like an IoT water level monitor can work perfectly on the bench (USB power) and die in the field in days.
Your options, in order of practicality:
- Power the board through the 3V3 pin from an efficient external regulator with sub-100 µA quiescent current (e.g. an HT7333 or MCP1700-series LDO), and desolder or cut the trace to the on-board power LED. Measure with a multimeter in series: you should see under 100 µA in deep sleep.
- Use a bare ESP32 module (ESP32-WROOM-32) on a minimal breakout with a good LDO — this is the honest "field device" approach and is what real deployments use.
- Use a board designed for battery use. Several ESP32 boards ship with proper power design (low-quiescent LDO, no always-on LED, sometimes a charger on board). They cost more; they earn it in the field.
Whatever you choose, measure the sleep current yourself before doing any battery-life arithmetic. The datasheet 10 µA is real — Espressif publishes it — but it only applies to the chip, not to your board.
Deep sleep: where your device should spend 99% of its life
The ESP32's deep sleep mode powers down the main CPUs, WiFi, Bluetooth and most RAM. What stays on: the RTC controller, RTC memory, RTC peripherals, and optionally the ULP coprocessor. Typical deep sleep current is about 10 µA with the RTC timer running (Espressif datasheet figure) — low enough that a coin cell could, in principle, hold the RTC for years.
Waking up is a full reset of the main CPUs: your program restarts from setup(). Plan for this — deep sleep is not a pause, it is a reboot with some memory preserved.
Wake sources
The RTC controller supports five wake triggers; the four you will actually use:
- Timer wake — wake after N microseconds. The workhorse for periodic sensing: wake, read, send, sleep.
- Touch pad wake — any configured touch pad crossing its threshold wakes the chip. Useful for a manual "report now" button on the enclosure.
- EXT0 wake — wake when one RTC GPIO goes high or low. One pin, one level: ideal for a water-level float switch or a PIR motion sensor in a flood monitoring node.
- EXT1 wake — wake when any of several RTC GPIOs changes. The multi-sensor version of EXT0 — for example a street-light controller waking on either a light sensor or a motion input, as in auto-intensity street light concepts.
Common mistake: calling
esp_deep_sleep_start()without configuring any wake source. The chip sleeps forever and only a reset or power cycle brings it back. Always verify a wake source is armed — a one-line test sketch that sleeps for 10 seconds and blinks an LED on wake will save you an afternoon.
Keeping state: RTC memory
Because deep sleep resets the CPUs, counters and configuration must live in RTC slow memory, declared with RTC_DATA_ATTR. This survives deep sleep (but not a power loss or hard reset). Typical uses: a boot counter, accumulated rainfall totals, or the last-known-good sensor reading to send if the current read fails.
The ULP coprocessor
The Ultra-Low-Power coprocessor can run while the main CPUs sleep, drawing roughly 150 µA typical. It can poll an ADC channel or a GPIO and wake the main CPU only when something interesting happens — for instance, watching a soil-moisture threshold so the main chip sleeps through a dry week and wakes only when irrigation is needed. Programming it is more work (limited instruction set, assembly or macro-based C), so treat it as an optimisation for version two of your device, not version one.
A minimal deep-sleep sketch
#include "esp_sleep.h"
RTC_DATA_ATTR int bootCount = 0;
void setup() {
bootCount++;
Serial.begin(115200);
// --- read battery voltage through a divider on GPIO34 ---
int raw = analogRead(34);
float battV = raw * (3.3 / 4095.0) * 2.0; // 100k/100k divider
Serial.printf("Boot %d, battery %.2f V\n", bootCount, battV);
// --- do your sensing and sending here (WiFi/LoRa) ---
// keep this section as short as possible
// --- sleep 10 minutes, wake on timer ---
esp_sleep_enable_timer_wakeup(10 * 60 * 1000000ULL);
Serial.println("Entering deep sleep");
Serial.flush();
esp_deep_sleep_start();
}
void loop() {
// never reached
}
Two notes on this sketch: the battery-voltage divider should use high-value resistors (100 kΩ or more) and ideally be switched — a permanent 100 kΩ divider leaks about 20 µA at 4.2 V, which is comparable to your sleep budget. And Serial.flush() before sleeping is not optional: without it the last log lines vanish because the UART dies mid-transmission.
Choosing the battery: 18650 vs LiPo pouch
For student field devices the realistic shortlist is two cell types:
18650 Li-ion cells are the default choice. Genuine cells from established manufacturers land around 2500–3500 mAh (a Samsung 35E is rated 3500 mAh; a 30Q is 3000 mAh). They are cheap, available everywhere, and fit standard holders. The catch: the market is flooded with fakes labelled "9900 mAh" that deliver 800 mAh and sag under load. Buy from a reputable electronics supplier, never from the cheapest marketplace listing, and weigh the cell — a genuine 18650 is about 45–48 g; the fakes are noticeably lighter.
LiPo pouch cells (the flat silver packs) give you more shape options and higher discharge rates, at the cost of fragility — puncture one and you have a fire. For a field device in a sealed box, 18650s in a proper holder are the safer, more serviceable choice.
Voltage behaviour you must design around:
- A Li-ion cell runs 4.2 V (full) down to about 3.0 V (empty — do not discharge below this; it damages the cell).
- The ESP32 itself runs down to about 2.3 V, but the brownout detector — which resets the chip when supply dips — trips in the roughly 2.4–3.0 V configurable band, and cheap boards wire it conservatively.
- Under a WiFi transmit burst (160–260 mA typical per the datasheet), a nearly-empty cell's voltage sags. If that sag crosses the brownout threshold, the chip resets mid-transmission, retries, and burns the battery faster — the classic "works at 80% charge, boot-loops at 20%" symptom.
- Practical rule: treat 3.2–3.3 V under load as your effective empty point, not 3.0 V. Size the battery so the device never lives near the bottom of the curve.
Regulator choice matters too. A linear regulator from 4.2 V to 3.3 V wastes (4.2 − 3.3) × current as heat — fine at sleep currents, noticeable during WiFi bursts. With a single cell you can also run the ESP32 directly from the battery through a low-dropout regulator chosen for sub-100 µA quiescent current, as discussed above. Avoid 5 V boost converters for battery devices: boosting 3.7 V to 5 V just to regulate back down to 3.3 V throws away 15–25% of your energy for no reason.
Charging safely: the TP4056 load-sharing trap
The TP4056 module is the standard cheap way to charge a single Li-ion cell from USB or a solar panel: it charges at up to 1 A (set by the programming resistor, 1.2 kΩ stock), terminates at 4.2 V, and the widely used variant adds a DW01 protection chip against over-discharge and short circuits. Wire battery to B+/B−, supply to IN+/IN−, and take your system power from OUT+/OUT−.
The classic student mistake is connecting the ESP32 load to the battery terminals (B+/B−) instead of the module output — or worse, to the charger input side. The TP4056 has no load sharing: it cannot distinguish current going into the battery from current drawn by your circuit. With the load on the battery terminals, the charger sees the load current as charge current, never detects the proper termination taper, and can keep pushing current into an already-full cell. The correct wiring is: charger → battery, and load from the protected OUT terminals, so the protection chip sits between battery and load.
The proper fix used in real products is a load-sharing (power-path) circuit: when external power is present, a P-channel MOSFET routes it directly to the load while the TP4056 charges the battery independently; when external power drops, the MOSFET switches the load to the battery. It is a handful of components and worth building on a small perfboard for any device that charges while running — which is exactly what a solar-topped-up node does. If that is beyond your current build, the acceptable student alternative is simpler: size the solar input so charging happens while the device is asleep (it is asleep 99% of the time anyway) and verify with a multimeter that charge current actually tapers near 4.2 V.
Two more safety notes that are not optional: never charge a Li-ion cell unattended on a breadboard with alligator clips, and put the cell in a proper holder — loose 18650s rolling around a project box short against each other. These are the failures that turn a project demo into an incident.
Choosing the radio: WiFi vs BLE vs LoRa
The radio is usually the hungriest part of the awake window. Current figures below are datasheet-typical values for the silicon involved; your board and antenna will move them somewhat, so treat them as comparison anchors, not guarantees.
| Radio | Typical active current | Sleep current | Range (practical) | Fit for |
|---|---|---|---|---|
| ESP32 WiFi (TX) | 160–260 mA (datasheet) | radio off in deep sleep | tens of metres | Devices near an access point; mains-adjacent or short deployments |
| ESP32 BLE | ~95–130 mA TX/RX typical | radio off in deep sleep | tens of metres | Phone-paired configuration, short data bursts to a gateway |
| LoRa (SX1276/SX1278 module) | ~20–120 mA TX depending on power setting (typ ~100 mA at +17 dBm), ~10 mA RX | ~0.2 µA | kilometres | Field sensors far from WiFi: farms, flood zones, air-quality grids |
The decision framework:
- Is there WiFi where the device will actually be deployed? A campus demo has WiFi; a farm field or riverbank does not. Students routinely develop against lab WiFi and discover at deployment that the device has nothing to join. Decide this before choosing hardware.
- How much data, how often? WiFi + MQTT is the right call for frequent, rich telemetry near infrastructure. For a sensor sending a few bytes hourly from a field, LoRa's seconds-long transmit at ~100 mA beats WiFi's association dance — WiFi can spend 3–8 seconds just joining the network and getting an IP, burning 160+ mA the whole time. That association overhead is why the "20 seconds awake" in our worked table is realistic for WiFi and pessimistic for LoRa.
- LoRa is not WiFi with longer range. It is low-bitrate (a few hundred bits per second at long range), duty-cycle limited by regulation, and needs a gateway. But for projects like a LoRa air-quality node or a farm sensor network, it is the technology that makes battery life measured in months possible.
- BLE's role in a field device is usually configuration: the installer pairs a phone, sets the WiFi credentials or LoRa keys, and never uses BLE again. Designing BLE as the primary data path for a remote sensor is almost always wrong.
If your project does use WiFi + MQTT, read the companion guide on connecting an IoT project to the cloud alongside the practical MQTT guide — and keep awake windows short with static IPs and persistent MQTT sessions where the broker supports them. For the broader power-supply picture (adapters, regulators, battery types), the batteries and regulators guide covers what this guide assumes. And if you are still choosing the controller itself, ESP32 vs Arduino vs Raspberry Pi walks through that decision.
Solar top-up: sizing it so the math works
A small solar panel does not need to run the device — it needs to replace the charge the device consumed. Work backwards from the daily energy budget:
- Daily consumption = average current × 24 h. Our 60-minute-interval example: ~1.01 mA × 24 ≈ 24 mAh per day.
- A nominal "5 V 1 W" panel delivers roughly 150–200 mA in full direct sun — treat 150 mA as the honest figure. But you do not get full sun all day: budget 3–4 equivalent full-sun hours for most of India outside monsoon, less in winter.
- Daily harvest ≈ 150 mA × 3.5 h ≈ 525 mAh — over 20× our 24 mAh budget. Even a 0.5 W panel covers it comfortably.
The real constraints are not panel size but the charging path: a bare panel's voltage swings wildly (a "5 V" panel hits 6–7 V open-circuit in bright sun), which a TP4056 tolerates on its input (up to 8 V) but which will destroy anything connected directly. Always charge through the charger module, never straight to the cell. And remember the load-sharing point from above: the device is asleep during almost all charging hours, so a simple TP4056-plus-panel arrangement works if you verify termination behaviour.
Monsoon reality check: during weeks of overcast sky, harvest can drop to 10–20% of the sunny figure. Size the battery to ride through 7–10 sunless days on its own — with our example's ~99-day battery life that is trivially satisfied, which is exactly why you do the battery-life math first and treat solar as the top-up, not the foundation.
Shrinking the awake window: the cheapest battery upgrade
From the battery-life table, every second shaved off the awake window pays off in every cycle for the life of the device — and unlike a bigger battery, it costs nothing. A WiFi device that spends 20 seconds awake per cycle is usually spending most of that time on overhead, not on your sensor. The typical breakdown:
- WiFi association and DHCP: 3–8 seconds. Assign a static IP to skip DHCP, and store the BSSID/channel of your access point so the ESP32 can associate directly instead of scanning. On a known network this routinely brings association under 2 seconds.
- Sensor warm-up: 1–5 seconds. Many gas and air-quality sensors need a warm-up period before readings stabilise. Read the sensor datasheet: if it specifies a 30-second preheat, you cannot cheat it — but you can power the sensor from a GPIO or a transistor-switched rail a few seconds before the ESP32 finishes its WiFi handshake, so warm-up overlaps with connection time instead of adding to it.
- TLS handshake: 2–4 seconds. If your cloud endpoint requires TLS (and it should for anything internet-facing), the handshake is a fixed cost per connection. A persistent MQTT session where the broker supports it, or batching several readings into one transmission, amortises this over more data.
- Retries on a flaky link: unbounded. This is the silent killer. A device at the edge of WiFi range can spend a minute retrying a failed publish, burning 160+ mA the whole time. Set a hard timeout on the awake window — if the publish has not succeeded in, say, 30 seconds, store the reading in RTC memory (or SPIFFS for larger backlogs) and go back to sleep. A missed report is a data gap; a dead battery is the end of the deployment.
A realistic target for a well-tuned WiFi cycle is 5–8 seconds awake. Plug 6 seconds into the worked table's 60-minute column: average current drops to about 0.3 mA and the same 2400 mAh cell stretches past 300 days on paper. Treat that as a design target to verify, not a promise — but it shows why firmware tuning beats battery upsizing.
Measuring current properly: verify, don't assume
Every number in your battery-life calculation should survive contact with a multimeter. The measurement setup is simple: break the positive lead between battery and board, and put the multimeter in series on a DC current range.
- Measure both states. Sleep current first (expect under 100 µA on a good board; milliamps on a stock dev board), then force a wake cycle and watch the awake current. Most handheld meters average too slowly to catch the WiFi transmit spikes — what you see is the average, which is exactly the number your battery-life math needs.
- Mind the meter's burden voltage. On the microamp range, the meter itself drops significant voltage, which can brownout the ESP32 and corrupt the measurement. If the device behaves differently with the meter in circuit, measure sleep current on the µA range and awake current on the mA range separately, or use a dedicated current-measurement tool.
- Measure the full cycle, not just the states. Time the awake window with serial logs (the
bootCountsketch above gives you this for free) and combine it with the measured currents. Calculated 6.0 mA average but measuring a 25-second awake window? Your battery life just dropped 20% — update the math, then fix the firmware. - Re-measure after every hardware change. A new sensor module, a different regulator, even a different USB cable during development — any of them can move the numbers. The students whose devices survive the field are the ones who measured twice.
Field-deployment checklist
Run through this before any device leaves the lab:
- Measured deep-sleep current with a multimeter in series: target under 100 µA including regulator and peripherals. If it reads milliamps, find the leaking component before anything else.
- Wake source verified with a 10-second test sleep, then the real interval.
- Battery voltage divider (if fitted) uses ≥100 kΩ resistors or is switched — confirm it is not doubling your sleep budget.
- Sensors and peripherals are powered from a GPIO or a switched rail, not left energised during sleep. Many sensor modules draw milliamps idle; cutting their VCC in software is often the single biggest win after the regulator fix.
- Brownout behaviour tested: run the cell down (or use a bench supply at 3.2 V) and confirm the device transmits cleanly without reset loops at the low end.
- The enclosure is weatherproofed to the level the deployment needs — a field device that survives electrically but drowns in the first rain is still a failure. Ventilate air-quality sensors; seal everything else.
- The radio choice matches the deployment site: WiFi credentials for the actual site network (not the lab's), or a LoRa gateway confirmed in range. Test at the site, not just the bench.
- Logging survives failure: keep the RTC boot counter and last battery voltage so a retrieved device tells you why it died.
Common failures and how to read the symptoms
| Symptom | Likely cause | Check |
|---|---|---|
| Battery dead in 1–3 days despite deep sleep code | Board-level drain: AMS1117 quiescent ~5 mA, power LED, USB-UART chip | Series multimeter measurement in sleep; expect <100 µA |
| Works on USB, resets on battery during transmit | Voltage sag under 160–260 mA WiFi burst trips brownout; weak/fake cell | Bench supply at 3.3 V; if stable, the cell or its internal resistance is the problem |
| Works at full charge, boot-loops below ~40% | Cell voltage sagging under load near brownout threshold | Treat 3.2–3.3 V under load as empty; use a genuine high-drain cell |
| TP4056 never finishes charging (LED stays red) | Load connected to battery terminals — charger sees load current as charge current | Rewire: battery to B+/B− only, load from OUT+/OUT− |
| Device never wakes up | No wake source armed before esp_deep_sleep_start() |
Add timer wake; test with a 10-second sleep first |
| Battery life far below the calculated figure | Awake window longer than assumed (slow WiFi join, retries) or self-discharge of a cheap cell | Time the awake window with logs; measure sleep current; verify cell capacity |
| Solar panel connected, battery still drains | Panel undersized for overcast weeks, or panel wired straight to cell/load instead of through charger | Measure charge current in real light; confirm TP4056 in the path; check the monsoon margin |
Putting it together
The design sequence for a battery-powered ESP32 field device is: pick the radio the deployment site actually supports → choose a board or regulator arrangement with sub-100 µA sleep → compute battery life from the duty cycle with honest, measured numbers → pick a genuine cell with margin for sunless weeks → charge it through a proper module with the load on the right terminals → verify every claim with a multimeter before the device leaves the lab.
Done this way, projects like flood monitoring or farm sensor networks stop being bench demos with a battery taped on and become genuine field devices — the kind that are still reporting three months later when you go back to collect them. More buildable IoT concepts in this space live in the IoT & Embedded branch hub.