An IoT startup sent me their prototype last spring. Three AA cells, a temperature sensor, a LoRa radio, and a target battery life of three years. The cells were dying in two weeks.
The hardware was fine. The firmware never slept. The MCU was running at full clock, polling the sensor in a tight loop, with the radio idle but powered. Once we got the system into proper sleep, average current dropped from 14 mA to under 30 µA. Three years was suddenly realistic on the same hardware.
That’s a pattern I’ve watched, and built into my own prototypes more than once. The instinct is to look at the battery first: maybe a bigger pack, maybe a different chemistry, maybe a primary cell instead of rechargeable. Almost always, the bigger lever is on the firmware side. The product wakes up too often, stays awake too long, or lights up things that don’t need to be lit.
Battery life is mostly a firmware problem. Here’s what I look at, in the order I look at it.
Sleep is not optional, and sleep needs cleanup
Modern MCUs idle in the sub-µA range when you put them properly to sleep. The keyword is properly. Calling deep_sleep() is not enough on its own. The MCU might be drawing 1 µA while the rest of the board pulls fifty times that, and the system current ends up looking nothing like the datasheet number.
A few things I always check before signing off on a low-power design.
Drive peripherals into reset before sleeping. Sensors, ADCs, level shifters, external flash, all of them. Many chips have an “idle” current that is dramatically higher than their actual reset current. If a part has a RESET pin, holding it low while the system is asleep often saves more current than any firmware tweak. If it has a chip-select, pull it to the inactive state. The datasheet’s standby section is worth reading slowly, then re-reading the fine print underneath it.
Size your pull-ups for sleep, not just for signal integrity. A 10 kΩ pull-up on a 3.3V rail draws 330 µA when the pin is held low. That single resistor can blow your entire sleep budget on three AAA cells. On lines that don’t need fast edges (I2C running at 100 kHz, configuration straps, button inputs), 100 kΩ or even 1 MΩ usually works fine. Run the math: V/R is your continuous loss while the line is in its active state. It’s an easy detail to miss when you’re focused on signal quality.
Plan the LEDs carefully. Most prototype boards carry at least one always-on indicator LED on a power rail. It draws two to five milliamps, and that’s perfectly fine during development. The trick is planning for the transition: power-rail LEDs, breakout headers, and test points are essential for design for testing, but a 2 mA always-on LED will eat the year of runtime you carefully designed into the system. I usually plan the BOM with two configurations from the start (development populate, production populate), or use 0Ω jumpers I can depopulate later. The prototype stays observable, the production unit stays efficient.
Interrupts, not polling
A microcontroller asleep waiting for an interrupt draws a few µA. The same microcontroller in a while(1) loop checking a register at full clock can draw 5 to 10 mA. Same job, three orders of magnitude difference.
Polling has its place (timing-critical loops, certain protocol implementations), but in a battery-powered product it’s one of the first things I look at. The model I aim for: the product is asleep by default, and only wakes for a specific event. Button press: GPIO interrupt. Sensor data ready: interrupt line from the sensor (most modern sensors have one, and using it pays for itself fast). Timer expired: RTC alarm wakes the MCU. The CPU does its work, goes back to sleep, repeat.
When a system is structured the other way (running by default, occasionally resting), most of the battery budget ends up funding idle time. Restructuring early is much cheaper than trying to claw current back at the end.
Turn the radio off
The wireless transceiver is almost always the biggest current sink on a connected product. WiFi typically draws 150 mA or more during transmission. BLE advertises around 5 to 15 mA depending on interval. Even LoRa can pull 40 mA during a transmit burst.
On an ESP32, the radio is the easiest place to leave current on the table. The chip can pull 30 to 80 mA just keeping the WiFi stack alive between transmissions. If your product sends data every five minutes, the radio should be fully off for four minutes and fifty-something seconds. Powering it down explicitly between transmissions (not just “stop sending”) is usually a one-day refactor that buys back a meaningful chunk of battery life.
A useful frame: how much time does the radio actually spend transmitting useful bits per hour? For most IoT sensors the answer is “a few hundred milliseconds”. The other 3,599+ seconds are an opportunity.
Pick the right protocol for the job
This is the decision that swings battery life the most, and it deserves a real conversation early in the project rather than a default that never gets revisited.
WiFi. High data rate, infrastructure already exists, easy to integrate. Tough on battery. Association is expensive (hundreds of ms at peak current), and even modem sleep sits in the hundreds of µA range. A great fit when the product is mains-powered, or when it lives on a charger between events.
ESP-NOW. Connectionless WiFi-layer protocol. No association handshake, so a transmit takes around 10 ms instead of a full second. Good middle ground when you control both ends and they share a building.
BLE. Designed for low power from the start. Advertising can be tuned to balance discoverability against current draw. Realistic to get years of life out of a coin cell for a sensor that wakes a few times per day. Range is the trade-off (tens of meters indoors).
LoRa / LoRaWAN. Kilometers of range, sub-µA standby, but slow (a few bytes per second) and bursty current draw during transmit. A strong fit for sensors that send small payloads occasionally over long distances. Not the right tool if you need to push images or stream data.
Match the protocol to the actual data the product needs to send and the actual range it needs to cover. The right answer usually becomes obvious once those two numbers are written down.
The landscape keeps moving
None of these numbers are fixed. New MCU families ship every year with better idle currents and smarter peripherals. Radio standards keep evolving: Thread and Matter on the smart-home side, Wi-Fi HaLow for long-range low-power IoT, LoRa pushing into the 2.4 GHz band, BLE 5.x getting more capable at mesh. The best choice two years ago might not be the best choice today, and what looks ideal on a new project this year will be worth a fresh look two or three years from now.
That’s why I try to anchor low-power design in principles rather than specific part numbers. The questions stay the same: who powers this, how often, what’s the minimum work the product really has to do, and how often does the radio truly need to speak? Get those clear and the technology can shift underneath without breaking the architecture. The numbers will move; the discipline of asking the right questions doesn’t.
Where I start on a new low-power project
Before writing a line of firmware, I sketch the duty cycle on paper. How long is the MCU awake per hour? How long is the radio on? What’s the average current that gives me, and what does that translate to in months or years on the chosen battery?
If the answer doesn’t fit, the architecture likely needs a rethink, and that’s much better to discover early than after the first prototype. The encouraging part is that with current MCU families and a well-chosen radio, “years on a small battery” is genuinely achievable. It just rarely happens by accident.
The battery is the easy part. The more interesting part is designing a product that spends most of its life doing nothing, on purpose.