An ESP32 battery powered sensor can run for months on a single cell, but only if the hardware and the firmware are designed together. This guide walks through board choice, regulators, battery monitoring and the publish cycle so your MQTT node wakes, reports and goes back to sleep with as little energy as possible.
Why Battery Life Is Mostly a Design Decision
The ESP32 is not a low-power chip in the way a microcontroller without a radio is. With Wi-Fi active it draws a large amount of current, and transmit bursts are the most expensive moments of its life. In deep sleep the chip itself drops to microamps. The difference between a node that lasts a week and one that lasts a season is how much time it spends in each state and how much the surrounding circuit wastes while the chip sleeps.
That gives you three levers: sleep current (usually dominated by the regulator, USB-serial chip and LEDs, not the ESP32), awake time (boot, Wi-Fi, DHCP, MQTT connect, publish) and wake frequency (halving the interval roughly doubles radio energy). The board is usually the biggest lever, so begin there.
Choosing a Board for an ESP32 Battery Powered Sensor
Development boards are built for convenience on a desk. They typically include an always-on USB-to-UART bridge, a power LED and a linear regulator chosen for price rather than quiescent current. Each of those can draw far more in sleep than the ESP32 does.
| Option | Sleep behaviour | Good for |
|---|---|---|
| Generic DevKit (ESP32-WROOM + AMS1117 + USB bridge) | Poor: regulator and USB chip draw milliamps continuously | Prototyping firmware only |
| Boards marketed as low-power (LiPo charger, low-Iq LDO, switchable USB) | Good: designed for deep sleep, often with a battery connector | Small runs and field pilots |
| Bare module (ESP32-WROOM / ESP32-C3 / ESP32-C6) on your own PCB | Best: you choose every part | Products and long deployments |
| ESP32-C3 or C6 based boards | Good, single core, lower active current than classic ESP32 in many workloads | Simple sensors that only need Wi-Fi |
When you evaluate a board, remove the power LED, check whether the USB-serial chip sits on the 3.3 V rail, and look up the regulator's quiescent current. Then measure sleep current with a multimeter in series with the battery or a current profiler; a USB power meter is not sensitive enough.
Regulators and Battery Chemistry
The ESP32 needs roughly 3.0–3.6 V and can pull short peaks of several hundred milliamps during Wi-Fi transmit. Your power path must deliver those peaks without browning out, and waste almost nothing while idle.
Single LiPo / Li-ion cell (3.0–4.2 V). The most common choice. Use a low-dropout regulator with a quiescent current in the low microamp range and enough headroom for transmit peaks. As the cell discharges towards 3.3 V the LDO drops out, so plan for the node to stop reporting reliably somewhere around 3.4–3.5 V cell voltage, and report a low-battery warning well before that.
LiFePO4 cell (around 3.2–3.3 V nominal). Its voltage sits inside the ESP32's operating range for most of the discharge curve, so some designs skip the regulator; check the maximum charge voltage against the absolute maximum ratings first.
AA cells. Alkaline cells sag under transmit peaks, especially in the cold. Pair them with a low-quiescent buck-boost converter, or use lithium primary cells.
Add a bulk capacitor (a few hundred microfarads) close to the module. It supplies the transmit bursts and prevents the brownout detector from resetting the chip, which is one of the most common causes of mysterious reboot loops on battery nodes.
Monitoring the Battery Voltage
A sensor that dies silently is worse than one that dies with a warning. Measure the cell with a voltage divider into an ADC pin and include it in every MQTT payload.
A divider of two equal high-value resistors (for example 2 × 1 MΩ) halves the cell voltage into the ADC range while drawing only microamps. High-value dividers have a high source impedance, so add a small capacitor (around 100 nF) from the ADC pin to ground to give the ADC a stable sample. If even that leakage matters, switch the divider's low side with a MOSFET driven by a GPIO.
On ESP32 Arduino core 2.x and 3.x, analogReadMilliVolts() applies the factory calibration and returns millivolts directly, which is far more accurate than scaling raw analogRead() values yourself. Average several samples, because single readings are noisy. On the classic ESP32, use an ADC1 pin (GPIO 32–39), since ADC2 is unavailable while Wi-Fi is active.
A Wake–Publish–Sleep Firmware Pattern
The firmware below is a complete Arduino sketch for a battery node. It wakes from deep sleep, reconnects to Wi-Fi using the channel and BSSID cached in RTC memory, publishes one JSON message with the battery voltage, disconnects cleanly and sleeps again. It uses the free public area of broker.connectmq.com, so pick a unique topic prefix and do not send anything sensitive.
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* TOPIC = "connectmq/demo/7f3a91/garden-node/telemetry";
const uint64_t SLEEP_SECONDS = 600; // publish every 10 minutes
const int BATTERY_PIN = 34; // ADC1 pin, divider 2 x 1 MOhm
const float DIVIDER_RATIO = 2.0;
RTC_DATA_ATTR uint32_t bootCount = 0;
RTC_DATA_ATTR uint8_t savedBssid[6];
RTC_DATA_ATTR int32_t savedChannel = 0;
WiFiClient net;
PubSubClient mqtt(net);
float readBatteryVolts() {
uint32_t sum = 0;
for (int i = 0; i < 16; i++) sum += analogReadMilliVolts(BATTERY_PIN);
return (sum / 16.0f) * DIVIDER_RATIO / 1000.0f;
}
bool connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.persistent(false);
if (savedChannel > 0) {
WiFi.begin(WIFI_SSID, WIFI_PASS, savedChannel, savedBssid);
} else {
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - start > 10000) {
savedChannel = 0; // force a full scan next time
return false;
}
delay(20);
}
savedChannel = WiFi.channel();
memcpy(savedBssid, WiFi.BSSID(), 6);
return true;
}
void goToSleep() {
mqtt.disconnect();
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
esp_sleep_enable_timer_wakeup(SLEEP_SECONDS * 1000000ULL);
esp_deep_sleep_start();
}
void setup() {
bootCount++;
float battery = readBatteryVolts(); // read before the radio loads the cell
if (!connectWifi()) goToSleep();
String clientId = "esp32-batt-" + String((uint32_t)ESP.getEfuseMac(), HEX);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setSocketTimeout(5);
if (!mqtt.connect(clientId.c_str())) goToSleep();
char payload[96];
snprintf(payload, sizeof(payload),
"{\"battery\":%.2f,\"boot\":%lu,\"rssi\":%d}",
battery, (unsigned long)bootCount, WiFi.RSSI());
mqtt.publish(TOPIC, payload);
mqtt.loop();
delay(50); // let the TCP stack flush
goToSleep();
}
void loop() {}
A few details matter here:
- The battery is read before Wi-Fi starts. Under transmit load the cell voltage sags, so a reading taken afterwards looks lower than reality.
- Every failure path ends in sleep. If Wi-Fi or the broker is unreachable, the node does not retry forever and drain the cell. It tries again on the next wake.
- Cached channel and BSSID let the radio skip the full scan, which shortens every wake. If the access point changes, the timeout clears the cache and the next boot scans normally.
- A static IP (via
WiFi.config()) removes the DHCP round trip as well. Use it when you control the network.
For a deeper look at sleep modes, wake sources and keeping state between wakes, see the ESP32 deep sleep MQTT guide.
Picking the Publish Interval
The right interval depends on how fast the value changes and how stale a reading may get.
| Measurement | Typical useful interval | Notes |
|---|---|---|
| Room or greenhouse temperature | 5–15 minutes | Changes slowly; average several samples per wake |
| Soil moisture | 30–60 minutes | Very slow; consider sampling more often but publishing only on change |
| Water tank level | 10–30 minutes | Add an immediate publish when a threshold is crossed |
| Door or motion | Event-driven | Wake on GPIO (ext0/ext1), not on a timer |
| Battery voltage | Every wake | Cheap to include; essential for maintenance |
Two techniques stretch the budget further. Batching: wake briefly without Wi-Fi to take a reading, store it in RTC memory, and only connect every N wakes to publish the batch as one JSON array. Report on change: compare the new value with the last published one in RTC memory and skip the radio entirely when it has not moved beyond a deadband. Always publish at least a periodic heartbeat so you can tell a quiet sensor from a dead one.
MQTT Settings That Save Energy
The protocol choices on the device side have a direct cost in airtime:
- QoS 0 for routine telemetry. QoS 1 adds a PUBACK round trip and keeps the radio on longer. Use it only for readings you cannot afford to lose; MQTT QoS levels explained covers the trade-off.
- Retained messages for the latest value. Publish telemetry with the retain flag and a dashboard that connects between wakes still sees the last reading immediately.
- Clean, short sessions. A sleeping node does not need a persistent subscription. Connect with a clean session, publish and disconnect.
- Compact payloads. Short JSON keys are enough at a few readings per hour.
- TLS costs a handshake. Encrypted connections on port 8883 take noticeably longer to establish than plain TCP on 1883 and keep the radio on longer. For production that trade-off is worth it, and ESP32 MQTT over SSL/TLS shows how to keep the handshake as cheap as possible.
Field Checklist Before Deployment
- Measure deep sleep current of the finished board.
- Log awake time per cycle and look for Wi-Fi retry outliers.
- Confirm the node survives a cold start at the lowest expected cell voltage without brownout loops.
- Publish battery voltage and a boot counter in every message.
- Set a dashboard alert on low battery voltage and on missing messages.
- Seal the enclosure but leave the antenna clear of metal and batteries.
- Move from the public test area to private credentials before installing anything outside your desk.
Moving From the Test Broker to Production
The free public MQTT broker at broker.connectmq.com is ideal while you tune intervals, because you can watch messages arrive with mosquitto_sub without signing up. For real deployments, switch to a private broker so nobody else can read your data. ConnectMQ gives each broker generated credentials and a client ID bound to them; only the connect call changes:
mqtt.setServer("broker.connectmq.com", 8883); // with WiFiClientSecure and the ISRG Root X1 CA
mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD");
In the ConnectMQ portal, the live topic explorer and threshold alert rules let you watch battery voltage across the fleet and get notified when a node drops below your replacement threshold. If you are building many nodes, the broader ESP32 IoT tutorial shows how the device side fits into a complete project, and the cloud MQTT broker guide explains what a hosted broker takes off your hands.
Get Started with ConnectMQ
Prototype your battery node against broker.connectmq.com today, then create a private broker on the free Serverless plan at app.connectmq.com with no credit card required. It is a fully managed MQTT service, so you spend your time on power budgets instead of broker maintenance.
Frequently Asked Questions
How long can an ESP32 battery powered sensor run on one LiPo cell?
It depends on the board's sleep current, the length of each wake and the publish interval. A board with a low-quiescent regulator and no always-on USB chip, waking every few minutes for a short publish, can run for months; a stock DevKit on the same cell may last only days.
Should I use deep sleep or light sleep for an MQTT sensor?
Use deep sleep when the node reports periodically and does not need to hold a connection. Light sleep keeps RAM and can maintain a Wi-Fi association, but its idle current is much higher, so it suits nodes that must react to commands quickly.
Why does my battery ESP32 keep rebooting when Wi-Fi starts?
That is usually the brownout detector reacting to the voltage dip during transmit bursts. Add a bulk capacitor near the module, use a regulator that can supply the peak current and check the battery's internal resistance, especially in the cold.
Can I measure battery voltage without wasting current?
Yes. Use a high-value divider with a small capacitor on the ADC pin, or switch the divider off with a MOSFET between readings, and read it with analogReadMilliVolts() on an ADC1 pin.



