How to Use ESP32 Deep Sleep with BLE Advertising

Your ESP32 BLE sensor drained its battery in two days. The math seemed simple: a small beacon, a coin cell, months of runtime. Instead, you’re recharging every weekend.
Here’s the uncomfortable truth: running BLE continuously pulls around 100mA. A 1000mAh battery lasts ten hours. But drop into deep sleep between broadcasts, and that same battery can last months.
If you landed here searching “wake on BLE” hoping to trigger your ESP32 with an incoming Bluetooth signal, that’s not how it works. The radio shuts off completely during deep sleep. But there’s a better pattern: wake on a timer, broadcast your data, go back to sleep. This guide walks you through the complete implementation.
Quick Concepts (Experienced Devs Can Skip)
Already comfortable with deep sleep and BLE advertising? Jump to “How the Pattern Works.”
Deep sleep is the ESP32’s lowest power state where your code can still wake it. The main CPU and most peripherals shut down, but the RTC (Real-Time Clock) memory and wake-up logic stay powered. Current draw drops from ~100mA to roughly 10µA, a 10,000x reduction.
BLE advertising means broadcasting small packets of data that any nearby device can receive. Unlike a BLE connection (two-way communication), advertising is one-way. Your ESP32 shouts “here’s my data!” and doesn’t care who’s listening. Perfect for sensors and beacons.
Why BLE can’t wake the ESP32: The Bluetooth radio is completely powered down during deep sleep. No radio means no listening for incoming signals. You need an external trigger, usually a timer, but also touch pins or GPIO interrupts from physical sensors.
How the Pattern Works
The cycle is straightforward:
[Deep Sleep] → [Timer Wake] → [Initialize BLE] → [Advertise for N seconds] → [Stop BLE] → [Deep Sleep]Timing matters. When the ESP32 wakes from deep sleep, it reboots. setup() runs fresh every time. This takes about 200-300ms before your code starts. Then BLE initialization adds another 100-200ms. Your advertising window needs to account for this startup cost.
For most beacon applications, you’ll advertise for 2-10 seconds, then sleep for 30 seconds to several minutes. The sleep duration depends on how fresh your data needs to be and how long your battery needs to last.
This approach is connectionless. You’re broadcasting to any listener, not pairing with a specific device. For bidirectional communication, you’d need a different pattern, but for sensor nodes, asset trackers, and beacons, this is exactly what you want.
Implementation: Complete Arduino Code
Prerequisites
- ESP32 board package installed in Arduino IDE (version 2.x or later)
- Any ESP32 development board (DevKit, NodeMCU-32S, etc.)
No additional libraries needed. The BLE functionality is built into the ESP32 Arduino core.
The Complete Sketch
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <esp_sleep.h>
// Configuration
#define DEVICE_NAME "ESP32-Sensor"
#define ADVERTISING_DURATION_MS 5000 // Advertise for 5 seconds
#define SLEEP_DURATION_US 60000000 // Sleep for 60 seconds (in microseconds)
BLEServer *pServer = nullptr;
BLEAdvertising *pAdvertising = nullptr;
void setup() {
Serial.begin(115200);
delay(100); // Give serial time to initialize
// Check what woke us up (useful for debugging)
esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
if (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER) {
Serial.println("Woke up from timer");
} else {
Serial.println("Initial boot or other wake source");
}
// Initialize BLE
BLEDevice::init(DEVICE_NAME);
pServer = BLEDevice::createServer();
pAdvertising = BLEDevice::getAdvertising();
// Configure advertising data
BLEAdvertisementData advertisementData;
advertisementData.setFlags(ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
advertisementData.setCompleteServices(BLEUUID((uint16_t)0x181A)); // Environmental Sensing Service
advertisementData.setName(DEVICE_NAME);
pAdvertising->setAdvertisementData(advertisementData);
pAdvertising->setScanResponse(false);
// Start advertising
pAdvertising->start();
Serial.println("Advertising started...");
// Advertise for the configured duration
delay(ADVERTISING_DURATION_MS);
// Stop advertising and prepare for sleep
pAdvertising->stop();
BLEDevice::deinit(false);
Serial.println("Advertising stopped, entering deep sleep...");
// Configure wake-up timer and enter deep sleep
esp_sleep_enable_timer_wakeup(SLEEP_DURATION_US);
esp_deep_sleep_start();
// Code never reaches here
}
void loop() {
// Empty - we never get here
// After deep sleep, the ESP32 reboots and runs setup() again
}Code Walkthrough
Why setup() does everything: Deep sleep triggers a full reboot. The loop() function never executes. We enter deep sleep at the end of setup(), then wake up and run setup() again from scratch.
The wake cause check: esp_sleep_get_wakeup_cause() tells you why the ESP32 woke up. Useful for debugging and for differentiating between initial power-on and timer wakes.
BLE deinitialization: Calling BLEDevice::deinit(false) before sleep cleanly shuts down the Bluetooth stack. The false parameter means “don’t release memory.” It doesn’t matter since we’re about to reset anyway, but it’s cleaner.
Sleep duration in microseconds: esp_sleep_enable_timer_wakeup() takes microseconds, not milliseconds. Sixty seconds is 60,000,000 microseconds. A common bug is passing milliseconds and wondering why your device wakes up 1000x faster than expected.
Customizing for Your Project
Adding Sensor Data to Advertisements
The BLE specification includes “Manufacturer Specific Data” for custom payloads. Here’s how to include a sensor reading:
// Read your sensor (example: temperature)
float temperature = readTemperatureSensor(); // Your sensor function
// Pack the data into the advertisement
BLEAdvertisementData advertisementData;
advertisementData.setFlags(ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
advertisementData.setName(DEVICE_NAME);
// Add manufacturer data (first two bytes are company ID, use 0xFFFF for testing)
uint8_t manufacturerData[4];
manufacturerData[0] = 0xFF; // Company ID low byte
manufacturerData[1] = 0xFF; // Company ID high byte
int16_t tempFixed = (int16_t)(temperature * 100); // Temperature as fixed-point
manufacturerData[2] = tempFixed & 0xFF;
manufacturerData[3] = (tempFixed >> 8) & 0xFF;
advertisementData.setManufacturerData(std::string((char*)manufacturerData, 4));Adjusting Timing
Shorter advertising windows save power but reduce the chance a scanner catches your broadcast. BLE advertising typically broadcasts every 100ms by default, so a 5-second window means roughly 50 advertisements.
For reliable detection with a phone app:
- Minimum: 2-3 seconds
- Recommended: 5-10 seconds
- Low-power extreme: 1 second (may miss some broadcasts)
Sleep duration depends on your application. A door sensor might sleep for 10 minutes since events are rare. A temperature logger might wake every 30 seconds for finer resolution.
Power Consumption and Battery Life
Let’s do the math with realistic numbers:
| State | Current | Duration | Charge Used |
|---|---|---|---|
| Active (advertising) | 100mA | 5 seconds | 0.139 mAh |
| Deep sleep | 10µA | 60 seconds | 0.00017 mAh |
| Total per cycle | 65 seconds | 0.139 mAh |
Average current: approximately 7.7mA
With a 1000mAh battery: ~130 hours (5.4 days)
Increase sleep time to 5 minutes:
| State | Current | Duration | Charge Used |
|---|---|---|---|
| Active | 100mA | 5 seconds | 0.139 mAh |
| Deep sleep | 10µA | 300 seconds | 0.00083 mAh |
| Total per cycle | 305 seconds | 0.140 mAh |
Average current: approximately 1.65mA
With a 1000mAh battery: ~606 hours (25 days)
The pattern is clear: sleep duration dominates. Longer sleep means dramatically longer battery life.
Troubleshooting Common Issues
BLE not showing up on your phone: Your advertising window might be too short. Scanner apps refresh every few seconds; if your window closes before the app scans, you’ll miss it. Try increasing ADVERTISING_DURATION_MS to 10000 (10 seconds).
ESP32 not sleeping: Make sure esp_deep_sleep_start() actually gets called. If you have an infinite loop or blocking code before it, sleep never happens. Add Serial prints to verify the flow.
Battery draining faster than calculated: Check the power LED on your dev board. It can draw 10-20mA constantly. For production, use a bare ESP32 module or desolder the LED. Also verify your voltage regulator efficiency; cheap boards waste significant current.
Erratic wake timing: The RTC timer isn’t perfectly accurate. Expect drift of a few percent. For precise timing, use an external RTC module.
Making This Work in Production
You now have the core pattern: timer wake, advertise data, deep sleep. This handles 90% of battery-powered BLE projects: temperature sensors, door monitors, asset beacons, and anything else that broadcasts periodic data.
For your next iteration, consider:
- Adding touch wake: ESP32 touch pins can wake from deep sleep, letting you trigger manual readings
- External interrupts: A reed switch or PIR sensor can wake the ESP32 on events, not just timers
- Hybrid approaches: Sleep with timer wake but also listen for GPIO events
The key insight remains: you can’t wake the ESP32 with Bluetooth, but you don’t need to. Periodic broadcasting covers most use cases and keeps your battery alive for months instead of days.
Hubble Network enables direct satellite connectivity for remote sensors—no gateways, no cellular coverage required. See how it works →