IoT Battery Life Optimization: Engineering Devices That Last Years on a Single Charge
"It should last at least a year on two AA batteries." Every IoT product manager says this. Very few embedded teams deliver it without a systematic power engineering process. After optimizing power budgets for client devices across agriculture, environmental monitoring, and industrial IoT, we have developed a repeatable framework.
This guide walks through the full process from power budget analysis to a real worked example showing how to achieve two-year battery life with a 1500 mAh cell.
Step 1: Build a Power Budget
Before writing a line of firmware, model your power consumption in a spreadsheet. Every power state, its current draw, and its duration must be accounted for.
A typical ESP32-based sensor node has these states:
| State | Current Draw | Duration per Cycle | |---|---|---| | Deep sleep | 10 µA | 599.5 s | | Boot + sensor warmup | 60 mA | 200 ms | | Sensor read | 20 mA | 100 ms | | WiFi connect | 180 mA | 1500 ms | | MQTT publish (TLS) | 140 mA | 500 ms | | WiFi disconnect + sleep entry | 30 mA | 200 ms |
Sampling cycle: 10 minutes (600 seconds).
Average current = Σ(I × t) / T_total
= (10µA × 599.5s + 60mA × 0.2s + 20mA × 0.1s + 180mA × 1.5s + 140mA × 0.5s + 30mA × 0.2s) / 600s
= (0.006 + 12 + 2 + 270 + 70 + 6) mAh / 600s
= 360 mC / 600s = 0.6 mA average
Battery life = 1500 mAh / 0.6 mA = 2500 hours ≈ 104 days
That is not two years. Let us fix it.
Step 2: Reduce Radio-On Time with Data Batching
WiFi connection and TLS handshake are expensive. The 1.5-second WiFi connect time is the largest single contributor to average current. Reduce radio-on time by batching readings.
Instead of connecting every 10 minutes, collect 12 readings in RTC memory and connect once per 2 hours to push them all.
#include "esp_sleep.h"
#include "esp_attr.h"#define READINGS_PER_BATCH 12
#define SLEEP_INTERVAL_SEC 600 // 10 minutes between samples
// Stored in RTC memory — survives deep sleep
RTC_DATA_ATTR sensor_reading_t reading_buffer[READINGS_PER_BATCH];
RTC_DATA_ATTR uint8_t buffer_count = 0;
RTC_DATA_ATTR uint8_t boot_count = 0;
void app_main(void) {
boot_count++;
// Always sample sensor
sensor_reading_t r;
read_sensor(&r);
reading_buffer[buffer_count++] = r;
if (buffer_count >= READINGS_PER_BATCH) {
// Upload all buffered readings, then reset
wifi_connect_and_push(reading_buffer, buffer_count);
buffer_count = 0;
}
// Return to deep sleep immediately
esp_sleep_enable_timer_wakeup((uint64_t)SLEEP_INTERVAL_SEC * 1000000ULL);
esp_deep_sleep_start();
}
Now WiFi connects every 2 hours instead of every 10 minutes — a 12× reduction in radio time.
Recalculated average current:
Total per 7200s cycle: ~780 mC
Average current = 780 mC / 7200s = 0.108 mA
Battery life = 1500 mAh / 0.108 mA = 13,888 hours ≈ 578 days ≈ 1.6 years
Good. Now push further.
Step 3: Peripheral Power Gating
Many sensors draw 1–5 mA continuously even when idle. Gate their power supply using a GPIO-controlled load switch or P-channel MOSFET.
#define SENSOR_POWER_PIN GPIO_NUM_4void sensor_power_on(void) {
gpio_set_level(SENSOR_POWER_PIN, 1);
vTaskDelay(pdMS_TO_TICKS(50)); // Sensor startup time
}
void sensor_power_off(void) {
gpio_set_level(SENSOR_POWER_PIN, 0);
}
// In your main sample routine
void sample_with_power_gating(sensor_reading_t *out) {
sensor_power_on();
read_sensor(out); // 100 ms
sensor_power_off(); // Sensor now draws 0 µA
}
A sensor drawing 2 mA continuously adds 2 mA × 8760h = 17,520 mAh per year — killing a 1500 mAh battery in under a month. Gating it saves almost all of that.
Step 4: LDO vs DCDC Regulator
If your battery is 3.7V LiPo and your MCU runs at 3.3V, you have two regulator choices:
LDO (e.g., AMS1117-3.3): Drop-out voltage ~1.2V. When battery is at 3.7V, efficiency = 3.3/3.7 = 89%. Quiescent current: 5–10 mA. In deep sleep where your MCU draws 10 µA, the regulator itself draws 100× more than the MCU.
DCDC Buck (e.g., TPS62840): Efficiency 85–95% across load range. Quiescent current: 60 nA. This single swap reduces your sleep-state regulator loss from 5 mA to 0.06 µA — a 83× improvement.
For any battery-powered IoT device, a DCDC converter with ultra-low quiescent current is non-negotiable.
Step 5: Deep Sleep Configuration
See our dedicated guide on [ESP32 deep sleep modes](/esp32-deep-sleep-battery-life) for the full breakdown. In summary: always use deep sleep (not modem sleep or light sleep) when the MCU has nothing to do. Keep sleep duration as long as your application latency requirement allows.
Final Budget: 2-Year Target
With data batching + peripheral gating + DCDC regulator:
Battery life = 1500 mAh / 0.12 mA = 12,500 hours = 521 days ≈ 1.75 years
Add a 10% battery derating for temperature and aging: 1500 × 0.9 = 1350 mAh effective.
1350 / 0.12 = 11,250 hours = 469 days ≈ 1.3 years
To reach the two-year target: extend batch interval to 4 hours, use a 2000 mAh cell, or reduce sensor warmup time by choosing a faster-starting sensor. All three are straightforward hardware and firmware choices.
The key insight: power optimization is mostly a design-time discipline. Fix it in the architecture, not after the hardware is locked.
[Contact Code Caracal](/contact) — we build production firmware for clients across 15+ countries.