Jump to: the power budget, the Arduino sketch, the ESPHome version, measuring current or troubleshooting.
Before you start
An ESP32 with Wi-Fi on draws somewhere around 100 mA on average, with peaks well above that while transmitting. A 2,000 mAh battery lasts less than a day. In deep sleep, the chip turns off the CPUs, Wi-Fi and most of its memory and draws around 10 µA, so the same battery could in principle last for years. A sensor that wakes up, takes a reading, sends it and goes back to sleep can run for months on batteries, which is how most battery-powered sensors work.
This guide explains how deep sleep behaves, works out a realistic power budget, builds the sketch in the Arduino IDE, shows the same device in ESPHome, and covers measuring the real current. You'll need:
- An ESP32 board. The board matters more than the chip: see choosing a board below.
- A sensor. The examples use a BME280 over I²C, wired as in the ESP32 BME280 Weather Station guide.
- The Arduino IDE 2.x with the Espressif ESP32 core 3.x installed as described in Espressif's installation guide, or ESPHome.
- A battery and a regulator suited to it, or a board with both built in.
- A multimeter that can measure current, ideally down to microamps.
What deep sleep actually does
Deep sleep isn't a pause. When the ESP32 wakes from it, the program starts again from the beginning: setup() runs, then loop(), just as after pressing reset. Ordinary variables are lost. Two things survive:
- RTC memory. Variables declared with
RTC_DATA_ATTRlive in a few kilobytes of memory that stays powered. Use them for a boot counter, the last value sent, or a Wi-Fi channel to skip scanning. - The RTC peripherals: the timer that wakes the chip, and on most ESP32 variants the ability to wake on a pin changing state.
So write the program as a straight line from top to bottom: read the sensor, connect, send, sleep. Most of the code usually lives in setup(), and loop() is never reached.
| Wake source | Use it for |
|---|---|
| Timer | Periodic readings, such as every 10 minutes |
| EXT0 (one RTC GPIO, high or low) | A button or a door contact |
| EXT1 (several RTC GPIOs) | Several buttons; the wake cause tells you which |
| Touch pad | Capacitive touch sensors, on chips that support them |
Only RTC GPIOs can wake the chip. On the original ESP32, these are GPIO0, 2, 4, 12 to 15, 25 to 27 and 32 to 39. The C3, S2 and S3 have their own lists, given in Espressif's documentation for each chip.
Choosing a board
The ESP32 chip draws about 10 µA in deep sleep. A typical development board draws far more, because of the parts around the chip, all of which stay powered:
- A power LED: often 1 to 5 mA, on its own enough to flatten a battery in weeks.
- The USB-to-serial chip, such as a CP2102 or CH340: often several milliamps if it's powered from the battery.
- A linear regulator with a high quiescent current. An AMS1117, common on cheap boards, draws around 5 mA doing nothing.
A generic DevKit board can draw 10 mA or more in "deep sleep", a thousand times what the chip needs. For battery projects, choose a board designed for low power: several makers sell ESP32 boards with a low-quiescent regulator, a battery connector and a switchable or absent LED. The ESP32-C3 and S3 boards with native USB, with no separate serial chip, also do well. Whatever you choose, measure it; datasheet numbers for the module don't include the rest of the board.
Sensor breakouts have the same problem. Many have their own regulator and power LED. A BME280 in forced mode sleeps at well under 1 µA, but the breakout around it may not. You can cut the LED trace, or power the sensor from a GPIO pin so it's off completely while the ESP32 sleeps.
The power budget
Battery life depends on the average current. Work it out from the time spent in each state:
| State | Current | Time per cycle |
|---|---|---|
| Awake, connecting to Wi-Fi and sending | about 120 mA | 3 s |
| Deep sleep, whole board | 25 µA | 597 s (rest of a 10-minute cycle) |
Average current = (120 mA × 3 s + 0.025 mA × 597 s) ÷ 600 s ≈ 0.62 mA. A 2,000 mAh battery would last 2,000 ÷ 0.62 ≈ 3,200 hours, about four and a half months. In practice, expect less: batteries lose capacity in the cold and don't deliver their full rating to the last milliamp-hour, and regulators waste some energy.
Two things stand out from the numbers:
- The awake time dominates. Here, three seconds awake uses about 24 times as much energy as ten minutes asleep. Cutting the wake time from 3 seconds to 1 nearly triples battery life. Halving the sleep current barely changes it.
- The interval is the other big lever. Reading every 30 minutes instead of every 10 cuts the awake energy by two-thirds. Ask how often the reading really needs to change.
The numbers above are illustrative. Measure your own awake time and sleep current and redo the sum, because both vary a lot between boards and networks.
Making the awake time short
Most of the wake time goes on Wi-Fi: scanning for the network, authenticating and getting an address from DHCP. Each of these can be cut:
- Use a static IP address. DHCP can take a second or more. Reserve an address for the sensor on your router and configure it in the sketch.
- Remember the channel and BSSID. Store the access point's channel and MAC address in RTC memory after the first connection, and pass them to
WiFi.begin()next time. The ESP32 then skips the scan. - Read the sensor before turning Wi-Fi on, so the radio isn't running while you wait for a measurement.
- Send one small message, then sleep straight away. Don't wait for anything you don't need.
- Give up quickly. If the network isn't reachable within a few seconds, go back to sleep and try next time, rather than draining the battery retrying.
For the shortest possible wake, look at ESP-NOW, Espressif's connectionless protocol. A message sent to a second ESP32 that's plugged in and acting as a gateway needs no association or DHCP at all, and the wake time drops to a fraction of a second.
The Arduino sketch
This sketch wakes every ten minutes, reads a BME280 in forced mode, sends the reading to an MQTT broker such as the one from the Mosquitto guide, and sleeps. Install the Adafruit BME280 and PubSubClient libraries from the Library Manager first.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_BME280.h>
#include "secrets.h" // WIFI_SSID, WIFI_PASS, MQTT_USER, MQTT_PASS
constexpr uint64_t SLEEP_SECONDS = 10 * 60;
constexpr uint32_t WIFI_TIMEOUT_MS = 6000;
const IPAddress LOCAL_IP(192, 168, 1, 60), GATEWAY(192, 168, 1, 1), SUBNET(255, 255, 255, 0);
const char *BROKER = "192.168.1.10";
RTC_DATA_ATTR uint32_t bootCount = 0;
RTC_DATA_ATTR int32_t savedChannel = 0;
RTC_DATA_ATTR uint8_t savedBssid[6];
Adafruit_BME280 bme;
WiFiClient net;
PubSubClient mqtt(net);
void goToSleep() {
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
esp_sleep_enable_timer_wakeup(SLEEP_SECONDS * 1000000ULL);
esp_deep_sleep_start();
}
void setup() {
bootCount++;
// 1. Read the sensor before the radio is on.
if (!bme.begin(0x76)) goToSleep();
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1, Adafruit_BME280::FILTER_OFF);
bme.takeForcedMeasurement();
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
// 2. Connect fast: static IP, and skip the scan when we know the channel.
WiFi.mode(WIFI_STA);
WiFi.config(LOCAL_IP, GATEWAY, SUBNET, GATEWAY);
if (savedChannel > 0) WiFi.begin(WIFI_SSID, WIFI_PASS, savedChannel, savedBssid);
else WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - start > WIFI_TIMEOUT_MS) {
savedChannel = 0; // force a full scan next time
goToSleep();
}
delay(20);
}
savedChannel = WiFi.channel();
memcpy(savedBssid, WiFi.BSSID(), 6);
// 3. Send one message, then sleep.
mqtt.setServer(BROKER, 1883);
if (mqtt.connect("garden-sensor", MQTT_USER, MQTT_PASS)) {
char payload[96];
snprintf(payload, sizeof payload,
"{\"temperature\":%.2f,\"humidity\":%.1f,\"boot\":%lu}",
temperature, humidity, (unsigned long)bootCount);
mqtt.publish("home/garden/climate", payload, true);
mqtt.loop();
mqtt.disconnect();
}
goToSleep();
}
void loop() {}
Create secrets.h in the sketch folder with four #define lines for your Wi-Fi name, Wi-Fi password and MQTT account, and keep it out of anything you share. Replace the addresses with ones from your own network: a free address outside your router's DHCP range for the sensor, your router as the gateway, and your broker's address.
How it works:
- Forced mode makes the BME280 take one measurement and return to sleep, instead of measuring continuously.
- The timeout caps the wake at six seconds when the network is down. It also clears the saved channel, in case the access point has moved channel.
- The message is retained (the
trueinpublish), so anything that subscribes later gets the last reading immediately instead of waiting up to ten minutes. bootCountsurvives sleep in RTC memory. Gaps in the count at the receiving end tell you how many wakes failed to send.
The broker's ACL must allow this account to publish to home/garden/#. Messages the broker rejects disappear without an error on the device.
The ESPHome version
ESPHome has a deep_sleep component, so the same device in ESPHome is mostly configuration:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
manual_ip:
static_ip: 192.168.1.60
gateway: 192.168.1.1
subnet: 255.255.255.0
mqtt:
broker: 192.168.1.10
username: garden-sensor
password: !secret mqtt_password
birth_message:
will_message:
deep_sleep:
id: sleeper
run_duration: 15s
sleep_duration: 10min
sensor:
- platform: bme280_i2c
address: 0x76
update_interval: never
temperature:
name: "Temperature"
id: garden_temp
on_value:
then:
- delay: 500ms
- deep_sleep.enter: sleeper
The device runs for at most run_duration, then sleeps. MQTT suits sleeping devices better than ESPHome's native API: Home Assistant marks an API device unavailable every time it sleeps, whereas MQTT retains the last values. The empty birth and will messages stop the device from being shown as offline between wakes.
ESPHome's start-up does more than the hand-written sketch, so wake times are longer, and battery life is shorter by the same proportion. To update the firmware over the air, the device has to be awake. Add a deep_sleep.prevent action triggered by a retained MQTT message, or hold the device awake with a jumper on a GPIO that you check at boot.
Measuring current
Estimates don't survive contact with real hardware. Measure both numbers in the power budget.
Sleep current: put a multimeter in series with the battery, on its µA or mA range, and wait until the board is asleep. Two warnings:
- A multimeter on a microamp range drops a significant voltage across itself (its burden voltage). When the ESP32 wakes and draws 100 mA or more, the voltage sags and the board may reset. Start the measurement on the mA range, and switch to µA only once the board is asleep. Some meters interrupt the circuit while switching, which resets the board; then you need a meter with a single auto-ranging current input.
- Disconnect USB. If the board is powered from both USB and the battery, the measurement is meaningless.
Awake time: the simplest method is to print millis() to the serial port right before esp_deep_sleep_start() during development. For the real current profile, including the transmit peaks, use a USB power meter with logging, a dedicated tool such as a Nordic Power Profiler Kit, or an oscilloscope across a small shunt resistor.
Batteries and regulators
- One lithium-ion or LiPo cell (3.0 to 4.2 V) is the usual choice. It needs a regulator to 3.3 V with low quiescent current and low dropout. Avoid the AMS1117. Regulators such as the MCP1700 or HT7333 draw a few microamps.
- Lithium iron phosphate (LiFePO4, 3.2 V nominal) can power the ESP32 directly, without a regulator, because its voltage stays within the chip's 3.0 to 3.6 V range for most of its discharge.
- Three AA alkaline cells start at 4.5 V, which is too high to connect directly. Two cells (3.0 V when fresh) fall below the ESP32's minimum quickly.
- Charging. Lithium cells need a proper charging circuit with protection. Many low-power boards include one. Don't charge lithium cells below freezing.
To report the battery level, measure its voltage through a resistor divider on an ADC pin. Use large resistors, such as two 1 MΩ, so the divider itself draws only a couple of microamps, and add a small capacitor (around 100 nF) across the lower resistor so the ADC reads a stable value.
Troubleshooting
The board resets or reboots in a loop when it tries to connect
A brownout. Wi-Fi transmission draws current peaks the battery or regulator can't supply. Add a 470 µF to 1,000 µF capacitor across the 3.3 V supply close to the module, and check the regulator's current rating.
Sleep current is in milliamps, not microamps
Something on the board stays powered: an LED, the USB-to-serial chip, the regulator or the sensor breakout. Remove things one at a time while measuring. GPIOs left floating can also leak; check the board's documentation for pins that need a defined state during sleep.
The serial monitor stops working after the first sleep
On boards with native USB, such as the C3 and S3, the USB port disappears during deep sleep and the computer drops the connection. Press reset with the BOOT button held to get a port back for uploading. Reading logs from a sleeping device is easier through a separate USB-to-serial adapter on the UART pins.
Readings arrive every ten minutes, then stop
The broker or Wi-Fi was unavailable, the sketch timed out, and the saved channel was wrong for the next attempt too. Check whether bootCount keeps rising in the readings that do arrive; if it does, the device is fine and failed wakes are the problem.
Next steps
- Mosquitto MQTT Broker Setup: the broker this sensor reports to, with an ACL for the account.
- ESPHome: Your First Device: the full ESPHome setup.
- ESP32 BME280 Weather Station: the same sensor, mains-powered, logging over USB.
- Espressif's sleep modes reference lists the wake sources and RTC GPIOs for each chip.