ESP32 and Arduino

ESP32 Deep Sleep MQTT: Battery-Powered Sensors That Publish and Sleep

Build a battery-friendly ESP32 sensor that wakes on a timer, connects quickly, publishes a retained reading, confirms it reached the broker and goes back to deep sleep.

Battery-powered ESP32 sensor node waking to publish in an esp32 deep sleep mqtt cycle
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

ESP32 deep sleep MQTT is how you get months instead of days out of a battery-powered sensor. In this tutorial you will build a node that wakes on a timer, reconnects to Wi-Fi as fast as possible, publishes a retained reading to broker.connectmq.com, confirms the message actually reached the broker, and goes back to sleep. Because the reading is retained, dashboards always show the last value, even though the device is offline almost all the time.

The Wake, Publish, Sleep Cycle

In deep sleep the ESP32 switches off its CPUs, most RAM and the radio. Only the RTC domain stays alive, consuming a tiny fraction of the active current. When the timer fires, the chip does not resume where it stopped: it reboots and runs setup() from the beginning. loop() never runs in this design.

Every wake cycle follows the same steps:

  1. Boot and read the sensor immediately, before the radio heats up the board.
  2. Connect to Wi-Fi, reusing the channel and access point saved from the previous cycle.
  3. Connect to the MQTT broker and publish the reading as a retained message.
  4. Confirm the broker received it.
  5. Disconnect cleanly, switch off the radio and enter deep sleep.

The time spent between steps 2 and 5 dominates energy use, because the Wi-Fi radio draws by far the most current. Every optimisation in this article aims to shorten that window or skip it when it would fail anyway.

Phase Relative power How to shorten it
Boot and sensor read Low to moderate Read sensor first, keep code small
Wi-Fi association and DHCP High Reuse channel and BSSID, optional static IP
MQTT connect and publish High Plain or session-light connect, single publish
Confirmation wait High Short timeout, give up quickly
Deep sleep Very low Board design: regulator, LEDs, USB chip

The last row is a frequent surprise: many development boards waste far more in their voltage regulator, power LED and USB-serial chip than the ESP32 itself uses in deep sleep. For real battery life, measure the board, not the chip. The companion article on battery-powered ESP32 sensors covers hardware choices and current measurements in more depth.

Why Retained Messages Are Essential Here

A sleeping sensor publishes once and disappears. If a dashboard or a phone app subscribes a minute later, a normal message is gone. With the retain flag set, the broker stores the last message on that topic and hands it to every new subscriber immediately. The dashboard shows the most recent reading the moment it opens, no matter when the device last woke up.

Put a timestamp or sequence number in the payload so consumers can tell how old the retained value is. A reading from four hours ago that looks current is misleading, and for a sleeping device "old" is the normal state. If retained messages are unfamiliar, the overview of what an MQTT broker does explains how the broker stores them.

Confirming Delivery Without QoS 1 Publish

PubSubClient can only publish at QoS 0, which means "sent", not "received". On an always-on device that is usually acceptable. On a sleepy device it is a real problem: if you call esp_deep_sleep_start() right after publish(), the radio can switch off while the packet is still in a buffer, and the message silently vanishes.

The sketch below uses a simple, reliable trick. It subscribes to its own state topic before publishing, then waits until the broker sends the new message back. Seeing your own message echoed proves the broker received and routed it. A sequence number stored in RTC memory distinguishes the fresh message from the old retained one, which the broker also delivers on subscribe.

The Complete ESP32 Deep Sleep MQTT Sketch

#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_sleep.h"

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* CLIENT_ID     = "sleepy-3e7f10";                 // unique
const char* TOPIC_STATE   = "connectmq/demo/sleepy-3e7f10/state";

const uint64_t SLEEP_SECONDS   = 300;                       // 5 minutes
const uint32_t WIFI_TIMEOUT_MS = 8000;
const uint32_t ACK_TIMEOUT_MS  = 3000;

// Survives deep sleep (lost on power-off or reset)
RTC_DATA_ATTR uint32_t seq = 0;
RTC_DATA_ATTR uint32_t failures = 0;
RTC_DATA_ATTR uint8_t savedBssid[6];
RTC_DATA_ATTR int32_t savedChannel = 0;
RTC_DATA_ATTR bool haveSaved = false;

WiFiClient net;
PubSubClient mqtt(net);
volatile bool acked = false;
char expected[24];

void goToSleep() {
  mqtt.disconnect();
  net.stop();
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  // Back off when things keep failing, so a dead network does not drain the battery
  uint64_t seconds = SLEEP_SECONDS * (failures > 3 ? 4 : 1);
  Serial.printf("Sleeping %llu s (failures=%lu)\n", seconds, (unsigned long)failures);
  Serial.flush();
  esp_sleep_enable_timer_wakeup(seconds * 1000000ULL);
  esp_deep_sleep_start();
}

float readSensor() {
  // Replace with your sensor; simulated value keeps the example self-contained
  return 20.0 + (esp_random() % 500) / 100.0;
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  // Our payload contains "seq":N; look for the one we just sent
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  if (msg.indexOf(expected) >= 0) acked = true;
}

bool connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.persistent(false);
  if (haveSaved) {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD, savedChannel, savedBssid, true);   // fast path
  } else {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  }
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - start > WIFI_TIMEOUT_MS) {
      haveSaved = false;              // AP may have changed channel: full scan next time
      return false;
    }
    delay(20);
  }
  memcpy(savedBssid, WiFi.BSSID(), 6);
  savedChannel = WiFi.channel();
  haveSaved = true;
  return true;
}

void setup() {
  unsigned long t0 = millis();
  Serial.begin(115200);
  float temperature = readSensor();                 // read before the radio warms the board
  seq++;

  if (!connectWifi()) { failures++; goToSleep(); }

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setSocketTimeout(4);
  if (!mqtt.connect(CLIENT_ID)) { failures++; goToSleep(); }

  mqtt.subscribe(TOPIC_STATE, 0);
  snprintf(expected, sizeof(expected), "\"seq\":%lu,", (unsigned long)seq);

  char payload[128];
  snprintf(payload, sizeof(payload),
           "{\"seq\":%lu,\"temperature\":%.2f,\"rssi\":%d,\"awakeMs\":%lu,\"failures\":%lu}",
           (unsigned long)seq, temperature, WiFi.RSSI(),
           millis() - t0, (unsigned long)failures);
  mqtt.publish(TOPIC_STATE, payload, true);         // retained

  unsigned long start = millis();
  while (!acked && millis() - start < ACK_TIMEOUT_MS) {
    mqtt.loop();
    delay(5);
  }
  if (acked) failures = 0; else failures++;
  Serial.printf("seq=%lu acked=%d awake=%lu ms\n", (unsigned long)seq, acked, millis() - t0);
  goToSleep();
}

void loop() {
  // Never reached: the ESP32 restarts in setup() after every wake-up
}

What each optimisation does

Channel and BSSID caching. A normal WiFi.begin() scans all channels to find the access point. Passing the channel and BSSID saved in RTC memory skips that scan, which is usually the single biggest saving. If the router changes channel, the fast path times out, the cache is cleared and the next wake performs a full scan.

Reading the sensor first. The radio warms the board noticeably, which skews temperature readings. Sampling before connecting avoids that.

Failure backoff. When the network is down, each wake burns energy on attempts that cannot succeed. After repeated failures, the node sleeps four times longer until a publish succeeds again.

awakeMs in the payload. Reporting how long the device was awake in the previous phase gives you a free, continuous view of energy cost. If it creeps up, something changed on the network.

No Last Will. A sleeping device disconnects on purpose every cycle, so an offline Last Will would be meaningless. Judge liveness by the age of the last retained reading instead.

Optionally, a static IP saves the DHCP exchange as well. Call WiFi.config(ip, gateway, subnet, dns) before WiFi.begin(), but only on networks where you control address assignment.

Verify It Works

Subscribe from your computer:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-3e7f10 -v -t "connectmq/demo/sleepy-3e7f10/#"

Shorten SLEEP_SECONDS to 20 for testing. You should see one message per cycle:

connectmq/demo/sleepy-3e7f10/state {"seq":7,"temperature":22.81,"rssi":-61,"awakeMs":1180,"failures":0}

Now stop the subscriber and start it again while the ESP32 is sleeping. The last reading appears immediately, which is exactly what a dashboard experiences. Then switch off your router for a few cycles: the serial monitor shows growing failures, and the first message after recovery reports how many cycles were missed.

The public area at broker.connectmq.com is a free public MQTT broker suitable for this kind of test. Its sessions expire after at most 30 minutes, which does not matter here because the sketch connects with a fresh session on every wake. When you move to a private ConnectMQ broker, the sketch only needs credentials (and TLS if you choose it); the only thing to watch is the free plan limit of 25 connections, which a fleet of sleepy sensors rarely reaches because each one is connected for only a moment per cycle.

Battery Life Checklist

  1. Remove or disable the power LED and use a board with a low-quiescent regulator, or power the module directly.
  2. Measure deep sleep current with a meter in series; it should be far below a milliamp on a well-designed board.
  3. Keep the awake window short: cached Wi-Fi, short timeouts, one publish.
  4. Choose a sensible interval. Halving the interval roughly doubles the radio-on energy; most environmental readings do not need minute-level resolution.
  5. Pull sensor power from a GPIO and switch it off before sleeping, so the sensor does not drain the battery in the background.
  6. Avoid TLS on every wake if battery life is critical and the threat model allows it on a private network; when you need encryption, budget the extra handshake time and follow ESP32 MQTT SSL with WiFiClientSecure.

Troubleshooting

  • Messages arrive only sometimes. You are sleeping before the publish leaves the radio. Keep the echo confirmation and do not shorten ACK_TIMEOUT_MS too much.
  • seq resets to 1 unexpectedly. RTC memory survives deep sleep but not a power cut, a brownout or pressing reset. A falling battery voltage causing brownouts is a common cause.
  • The device never wakes. Check that you call esp_sleep_enable_timer_wakeup() with microseconds and a 64-bit value; a 32-bit overflow produces a much shorter or wrong interval.
  • Serial output is cut off. Call Serial.flush() before sleeping, as the sketch does.
  • Long awake times on some cycles. The fast Wi-Fi path failed and a full scan ran. Check that your router uses a fixed channel.

Running Sleepy Sensors in Production

Retained values are only as dependable as the broker holding them. A managed MQTT broker keeps that endpoint available while your sensors sleep, and the same retained topics serve your dashboards, scripts and apps. With ConnectMQ, you create per-device credentials in the portal, see each sensor's last retained value in the live topic explorer and set threshold alert rules on readings such as temperature. A cloud MQTT broker also means sensors in a field, a cellar and a warehouse can report to one endpoint without any local server. New to the board? Start with the ESP32 IoT tutorial.

Get Started with ConnectMQ

Create a free Serverless broker at app.connectmq.com with no credit card. Point your sleepy sensors at it with their own credentials and watch retained readings appear in the portal as they wake.

Frequently Asked Questions

Why does my ESP32 lose MQTT messages before deep sleep?

QoS 0 publish returns as soon as the data is queued, so sleeping immediately can cut it off. Wait for a confirmation, such as the echo technique in this sketch, then disconnect and sleep.

Should a deep sleep sensor keep an MQTT session?

Usually not. A fresh connection each wake is simpler and works with any broker. Persistent sessions only help if the device must receive commands queued while it slept.

How can a sleeping ESP32 receive commands?

Publish commands as retained messages on a topic the device subscribes to when it wakes. It receives the latest command immediately, acts on it, then clears it by publishing an empty retained message.

What is a good publish interval for a battery sensor?

It depends on how fast the measured value changes. Environmental sensors often work well with intervals of five to fifteen minutes; shorter intervals cost proportionally more energy.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.