Ultra-Low Power IoT: How I Extended Battery Life from Days to Months
Practical techniques for squeezing months of battery life from your IoT devices - with real numbers from Tracer2 and Plant-Bot

Ultra-Low Power IoT: How I Extended Battery Life from Days to Months
Let me tell you about the moment I realized I had a power problem.
My first version of Tracer2 - a wearable sensor I was building - lasted exactly 4 days on a coin cell battery. The math said it should last 30 days. Something was very wrong.
After a week of measuring, cursing, and measuring again, I found the culprits: a 10kΩ pull-up resistor wasting 330µA, an always-on sensor I forgot to power-gate, and a voltage regulator with 5mA quiescent current. Together, they were eating through my battery 7x faster than the actual microcontroller.
This post is everything I learned fixing that - and the techniques I now use on every battery-powered project.
Why Power Optimization Matters More Than You Think
Here's a truth that took me too long to accept: poor power management kills products.
A device that needs weekly charging gets abandoned. A remote sensor requiring monthly battery swaps becomes a maintenance nightmare. But with careful design, you can achieve:
- Months from a coin cell: Tracer2 now gets 53 days from a CR2450
- Years from AA batteries: Theoretically achievable with proper optimization
- Indefinite runtime with solar: Plant-Bot has been running 6+ months without intervention
The good news? Most of this isn't rocket science. It's attention to detail.
The Power Budget: Your Most Important Spreadsheet
Before you write a single line of code, you need a power budget. I've made the mistake of skipping this step exactly once. Never again.
Understanding Power vs Energy
Quick refresher on the basics:
Power (W) = Voltage (V) × Current (A)
Energy (Wh) = Power (W) × Time (h)
Battery life = Battery capacity (mAh) / Average current (mA)
The key insight: average current determines battery life, not peak current. A device drawing 100mA for 1 second then sleeping at 10µA for 999 seconds has an average current of ~110µA - plenty manageable for a coin cell.
My Actual Power Budget for Tracer2
Here's the real spreadsheet I used:
Component | Current | Duty Cycle | Average
-------------------|---------|------------|--------
ESP32-C6 (sleep) | 7µA | 99% | 6.9µA
ESP32-C6 (active) | 80mA | 0.5% | 400µA
LSM6DSLTR IMU | 600µA | 0.5% | 3µA
BLE transmission | 20mA | 0.1% | 20µA
VL53L0CX ToF | 19mA | 0.2% | 38µA
AHT20 sensor | 200µA | 0.1% | 0.2µA
Total average: | 468µA
CR2450 capacity: 600mAh
Battery life: 600mAh / 0.468mA = 1282 hours = 53 days
I always add a 50% safety margin for real-world conditions, so I target ~1 month practical battery life. Tracer2 consistently hits that mark.
How to Measure Real Power Consumption
Datasheet numbers lie. Okay, they don't lie exactly, but they're measured under ideal conditions that don't match reality. You need to measure your actual device.
What you need:
- A multimeter with µA range (essential)
- Power Profiler Kit II from Nordic or Joulescope (nice to have)
- Patience (non-negotiable)
What to measure:
- Sleep current: Use µA-capable meter, measure for 30+ seconds to catch any periodic wakes
- Active current: Capture both peak and average
- Total energy per cycle: Integrate current over a complete wake/sleep cycle
I've seen people skip measurements because their calculations "looked fine." Don't be that person. Measure everything.
Hardware: Where Power Optimization Starts
The best firmware optimization can't fix bad hardware choices. Here's where to focus.
Voltage Regulators: The Silent Power Killer
This was my most expensive lesson. A bad voltage regulator can waste more power than your entire MCU.
The AMS1117 disaster:
Regulator quiescent: 5mA
ESP32 sleep: 0.007mA
Total: 5.007mA
The regulator wastes 99.86% of your power budget!
What to use instead:
- Good: AP2112 (55µA quiescent) - my default choice
- Better: MCP1700 (1.6µA quiescent)
- Ultra-low: TPS62840 (60nA quiescent) - for when every nanoamp matters
Rule of thumb: If your regulator's quiescent current is more than 10% of your average system current, find a better regulator.
Component Selection: Low-Power by Default
Not all sensors are created equal. Here's what I've learned:
Temperature sensors:
- ✅ AHT20: 200µA active, <1µA sleep
- ❌ DHT22: 1mA active, always-on (no sleep mode!)
IMUs:
- ✅ LSM6DSLTR: 600µA with embedded features
- ⚠️ MPU6050: 3.6mA typical - usable but power-hungry
When choosing sensors, look for:
- True sleep/shutdown modes
- I2C interface (lower pin count = lower leakage)
- Embedded processing (pedometer, gesture detection reduce MCU wake time)
- Interrupt outputs (wake on event instead of polling)
Power Gating: Turn Off What You're Not Using
Power gating is one of the highest-impact optimizations you can make. The concept is simple: use a transistor or load switch to completely cut power to peripherals when they're not needed.
// Power on soil moisture sensor
digitalWrite(SOIL_POWER_PIN, HIGH);
delay(100); // Stabilization time
int moisture = analogRead(SOIL_ADC_PIN);
digitalWrite(SOIL_POWER_PIN, LOW); // Power off immediately
// Savings: 10mA continuous → 2.8µA average (100ms per hour)Load switch options:
- P-channel MOSFET: Simple, cheap, ~100mV dropout
- Dedicated IC (TPS22860): Lower on-resistance, cleaner switching
- GPIO direct: Works for <12mA (ESP32's limit)
Plant-Bot uses power gating on its soil moisture sensor. Without it, that single sensor would drain the battery in 8 days. With gating, it contributes less than 7µAh per day.
Pull-up Resistors: Death by a Thousand Cuts
This one got me on Tracer2. A 10kΩ pull-up resistor between 3.3V and ground wastes 330µA. That's more than the entire rest of my sleep current budget!
The fix:
3.3V across 10kΩ = 330µA wasted ❌
3.3V across 100kΩ = 33µA ✅
Or use internal pull-ups (~45kΩ on ESP32)
Go through your schematic and audit every resistor connected to a power rail. You might be surprised what you find.
Firmware: Make Every Microsecond Count
With good hardware in place, firmware optimization is where you squeeze out the remaining efficiency.
Deep Sleep: Your Default State
For IoT sensors, deep sleep should be where your device spends 99%+ of its time.
ESP32 sleep modes compared:
| Mode | Current | Wake Time | Best For |
|---|---|---|---|
| Active | 80mA | - | Processing |
| Modem sleep | 20mA | <1ms | WiFi off briefly |
| Light sleep | 800µA | ~3ms | Short pauses |
| Deep sleep | 7µA | ~300ms | Long sleep |
| Hibernation | 2µA | ~300ms | Ultra-low power |
For Tracer2 and Plant-Bot, deep sleep is the answer 99% of the time. The 300ms wake time is negligible compared to the power savings.
Wake-up Strategies That Actually Work
Timer wake-up: For regular sensing intervals
esp_sleep_enable_timer_wakeup(3600 * 1000000ULL); // 1 hour
esp_deep_sleep_start();GPIO interrupt: For event-driven wake (motion sensor, button press)
esp_sleep_enable_ext0_wakeup(IMU_INT_PIN, 1); // Wake on motionCombined approach (what I use on Tracer2):
// Wake on IMU interrupt OR timer (whichever comes first)
esp_sleep_enable_ext0_wakeup(IMU_INT_PIN, 1);
esp_sleep_enable_timer_wakeup(3600 * 1000000ULL); // 1 hour backup
// Check what woke us up
esp_sleep_wakeup_cause_t reason = esp_sleep_get_wakeup_cause();
if (reason == ESP_SLEEP_WAKEUP_EXT0) {
handleMotionEvent();
} else if (reason == ESP_SLEEP_WAKEUP_TIMER) {
handlePeriodicWake();
}The LP Core Advantage (ESP32-C6)
If you're using the ESP32-C6, you have access to something special: the LP (Low-Power) core. This secondary RISC-V core runs at 20MHz and can operate while the main core sleeps.
On Tracer2, I use the LP core to monitor the IMU for motion events. Instead of waking the main core every few seconds to check for movement, the LP core handles it at ~15µA - a 5x improvement over polling.
Use cases where the LP core shines:
- Continuous sensor monitoring
- Analog threshold detection
- Simple state machines
- Timestamp maintenance
Minimize Wake Time
Every second your device is awake costs power. A lot of power.
80mA for 10 seconds: 222µAh
80mA for 1 second: 22µAh
That's 10x power savings for the same operation!
The wrong way:
wakeup();
connectWiFi(); // 5 seconds
readSensor1(); // 1 second
readSensor2(); // 1 second
sendData(); // 3 seconds
sleep(); // Total: 10 seconds awakeThe right way:
wakeup();
powerOnAllSensors(); // Start sensors warming up
connectWiFi(); // WiFi connects while sensors stabilize
readAllSensors(); // Sensors are ready (parallel operation)
sendData();
sleep(); // Total: 6 seconds (40% less wake time)WiFi: The Power Monster
WiFi is almost always the biggest power consumer. Minimize connection time ruthlessly.
Use static IP instead of DHCP:
// DHCP: 3-5 seconds to connect
WiFi.begin(ssid, password);
// Static IP: 1-2 seconds (50% faster)
WiFi.config(local_IP, gateway, subnet);
WiFi.begin(ssid, password);Other WiFi optimizations:
- Cache the WiFi channel from last connection
- Specify BSSID to skip scanning
- Always set connection timeouts (I use 10 seconds max)
- Consider WiFi 6 TWT on ESP32-C6 (15% power savings on Plant-Bot)
BLE: Often the Better Choice
For short data transmissions, BLE is dramatically more efficient than WiFi:
WiFi: 80-120mA for 3-10 seconds = 67-333µAh per transmission
BLE: 15-25mA for 0.5-2 seconds = 2-14µAh per transmission
BLE is ~20x more efficient for small data packets!
Tracer2 uses BLE exclusively. Plant-Bot uses WiFi because it needs to send data to a cloud service, but I've implemented aggressive timeouts and TWT to minimize the impact.
Real-World Results
Let me show you what these techniques achieved on actual shipping products.
Tracer2: 53 Days from a Coin Cell
Initial design (before optimization):
- Always-on IMU: 600µA
- BLE advertising: 500µA average
- Periodic wake every 10 minutes: 200µA average
- Total: 1300µA → 19 days ❌
Optimized design:
- Motion-triggered wake (IMU interrupt): 3µA
- LP core monitoring instead of full wake: 15µA
- BLE 5.3 connection subrating: 50µA
- Deep sleep: 7µA
- Periodic wake every 1 hour: 400µA average
- Total: 475µA → 53 days ✅
Key optimizations that made the difference:
- GPIO interrupt wake instead of polling
- ESP32-C6 LP core for IMU monitoring
- BLE 5.3 connection subrating
- Reduced wake frequency (10 min → 1 hour)
- Optimized wake time (15s → 5s)
Plant-Bot: 6+ Months on Solar
Power budget:
- Deep sleep: 8µA (99.9% of time)
- Wake cycle: 120mA for 15 seconds per hour
- Average: 508µA
Daily energy math:
- Consumption: 508µA × 24h × 3.7V = 45mWh
- Solar generation (sunny day): ~5000mWh
- Margin: 111x on sunny days ✅
Field results in Seattle (6 months):
- Summer: Battery 95-100% daily
- Winter: Battery 60-85% (shorter days, more clouds)
- Longest cloudy stretch: 8 days, battery minimum 52%
- Uptime: 100% - no manual intervention needed
Common Mistakes I've Made (So You Don't Have To)
Mistake 1: Using delay() When You Should Sleep
// ❌ BAD: delay() keeps the CPU active at 80mA
digitalWrite(LED, HIGH);
delay(1000);
digitalWrite(LED, LOW);
// ✅ GOOD: Light sleep during the wait
digitalWrite(LED, HIGH);
esp_sleep_enable_timer_wakeup(1000000);
esp_light_sleep_start(); // ~800µA instead of 80mA
digitalWrite(LED, LOW);Mistake 2: Not Power-Gating Sensors
Plant-Bot's soil moisture sensor draws 10mA when active. Without power gating:
- Always on: 10mA × 24h = 240mAh/day (battery dead in 8 days)
- Power-gated: 10mA × 0.1s × 24 = 6.7µAh/day ✅
That's a 35,000x difference.
Mistake 3: Choosing the Wrong Regulator
I mentioned this earlier, but it's worth repeating because it's so common. Check the quiescent current of your regulator before anything else.
Mistake 4: Not Measuring Real Power
Theoretical calculations are a starting point, not the finish line. I've been surprised by unexpected power draws more times than I care to admit. Measure your actual device under realistic conditions.
Your Low-Power Checklist
Hardware:
- LDO quiescent current <10µA
- Sensor power gating implemented
- Pull-up resistors >10kΩ (or use internal)
- No floating pins
- Battery voltage monitoring
Firmware:
- Deep sleep between measurements
- Wake time minimized (<10 seconds)
- WiFi connection timeout
- Sensors powered off after reading
- Battery level checking
Validation:
- Sleep current measured (<50µA)
- Wake current measured
- Complete cycle energy calculated
- Battery life validated (>1 week real-world test)
The Bottom Line
Ultra-low power design isn't magic. It's systematic attention to detail:
- Start with a power budget
- Choose components with low-power modes
- Power-gate everything you can
- Sleep aggressively
- Measure obsessively
These techniques took Tracer2 from 4 days to 53 days of battery life. They keep Plant-Bot running indefinitely on solar power. And they'll work for your projects too.
What's Next?
Want to dive deeper? Check out these related posts:
- Building Tracer2: ESP32-C6 Wearable - Full build log
- Solar-Powered IoT Tutorial - Plant-Bot deep dive
- ESP32-C3 vs C6 Comparison - Which chip for your power needs?
Got a power optimization challenge you're stuck on? Share your power budget in the contact form and I'll take a look.