The ESP32 MQTT Last Will feature lets the broker announce on your device's behalf that it has gone offline, even when the device crashed or lost power and could not say goodbye itself. In this tutorial you will configure a retained Last Will, a matching birth message and a clean shutdown message, so every dashboard and script sees accurate online/offline status at all times.
What Last Will and Testament Does
When an MQTT client connects, it can hand the broker a message to publish later: a topic, a payload, a QoS level and a retain flag. That is the Last Will and Testament (LWT). The broker stores it and publishes it only if the client disappears without a proper DISCONNECT packet. That covers exactly the cases you care about:
- The device lost power or browned out.
- The firmware crashed, hung or was reset by a watchdog.
- Wi-Fi dropped and the keepalive timer expired.
- The network between device and broker failed.
If the client disconnects cleanly, the broker discards the will. That is why a clean shutdown needs its own "offline" publish, described further down.
On its own, a will only announces death. Pair it with a birth message, a retained "online" publish sent right after each successful connect, and the status topic always holds the current truth: online while the device is connected, offline from the moment the broker notices it is gone.
The Status Topic Pattern
| Event | Who publishes | Topic | Payload | Retained |
|---|---|---|---|---|
| Device connects | ESP32 (birth) | connectmq/demo/f5c820/<device>/status |
online |
Yes |
| Device dies unexpectedly | Broker (will) | same | offline |
Yes |
| Device shuts down cleanly | ESP32 | same | offline |
Yes |
Both messages must be retained. Without the retain flag, a dashboard that starts after the device came online would never learn its state, because the birth message is gone. With it, every new subscriber receives the latest status immediately.
Use the same topic for birth and will, and keep payloads simple. Plain online and offline work with Home Assistant availability topics, Node-RED switches and most dashboards without any parsing. If you want more detail, use JSON for both, for example {"state":"online","fw":"1.2.0"}.
ESP32 MQTT Last Will With PubSubClient
PubSubClient sets the will through an overload of connect():
boolean connect(const char* id,
const char* user, const char* pass,
const char* willTopic, uint8_t willQos,
boolean willRetain, const char* willMessage);
For anonymous connections, such as the public area of broker.connectmq.com, there is a variant without user and pass. Here is a complete sketch that publishes the birth message, keeps the status accurate and shuts down cleanly when you press the BOOT button:
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const int SHUTDOWN_BUTTON = 0; // BOOT button
WiFiClient net;
PubSubClient mqtt(net);
String deviceId, statusTopic, telemetryTopic;
unsigned long lastPublish = 0;
void connectMqtt() {
while (!mqtt.connected()) {
bool ok = mqtt.connect(deviceId.c_str(),
statusTopic.c_str(), // will topic
1, // will QoS
true, // will retain
"offline"); // will payload
if (ok) {
mqtt.publish(statusTopic.c_str(), "online", true); // birth message
Serial.println("Connected, status=online");
} else {
Serial.printf("Connect failed, state=%d\n", mqtt.state());
delay(3000);
}
}
}
void cleanShutdown() {
mqtt.publish(statusTopic.c_str(), "offline", true);
mqtt.loop();
delay(100);
mqtt.disconnect(); // the broker discards the will
WiFi.disconnect(true);
Serial.println("Clean shutdown done");
esp_deep_sleep_start(); // sleep until reset
}
void setup() {
Serial.begin(115200);
pinMode(SHUTDOWN_BUTTON, INPUT_PULLUP);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
deviceId = "esp32-lwt-" + String((uint32_t)ESP.getEfuseMac(), HEX);
statusTopic = "connectmq/demo/f5c820/" + deviceId + "/status";
telemetryTopic = "connectmq/demo/f5c820/" + deviceId + "/telemetry";
mqtt.setServer("broker.connectmq.com", 1883);
mqtt.setKeepAlive(15); // seconds; the broker detects loss after about 1.5x this
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (digitalRead(SHUTDOWN_BUTTON) == LOW) cleanShutdown();
if (millis() - lastPublish > 10000) {
lastPublish = millis();
String payload = "{\"uptime\":" + String(millis() / 1000) + "}";
mqtt.publish(telemetryTopic.c_str(), payload.c_str());
}
}
Testing All Three Cases
Subscribe to every status topic under your prefix from a computer:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-f5c820 -t 'connectmq/demo/f5c820/+/status' -v
Then try each scenario:
- Normal start. Power the board. You see
.../status onlinewithin a few seconds. - Crash or power loss. Pull the USB cable. After roughly one and a half keepalive intervals (about 20–25 seconds with a 15-second keepalive), the broker publishes
.../status offline. - Clean shutdown. Power it back on, wait for
online, then press BOOT.offlineappears immediately, published by the device itself.
Stop mosquitto_sub and start it again while the device is in any state. Because both messages are retained, you instantly receive the current status. That is the behaviour your dashboards depend on.
Tuning the Keepalive
The keepalive interval decides how fast the will fires after a silent failure. The broker waits about 1.5 times the keepalive with no packets from the client before it gives up on the connection.
| Keepalive | Approximate time to "offline" | Trade-off |
|---|---|---|
| 5 s | under 10 s | Fast detection, more ping traffic, false offline on poor Wi-Fi |
| 15 s (PubSubClient default) | about 20–25 s | Good default for mains-powered devices |
| 60 s | about 90 s | Less traffic, slow detection |
A short keepalive only works if mqtt.loop() runs frequently. If your loop blocks for longer than the keepalive, the broker drops a healthy device and fires the will, and you will see status flapping between online and offline. Long blocking calls also lead to resets, as explained in fixing ESP32 task watchdog resets.
Common Mistakes
Forgetting the birth message. Without it, the status stays offline after a reconnect, because the will from the previous failure is still retained on the topic.
Not retaining the will. A non-retained will reaches only subscribers that are connected at that moment. Anything that starts later sees the last retained value, which is probably online, and believes a dead device is alive.
Sharing one status topic between devices. Each device needs its own status topic, usually with the client ID or device name in the path. A shared topic only shows whichever device spoke last.
Expecting the will on a clean disconnect. mqtt.disconnect() tells the broker the client is leaving on purpose, so the will is discarded. Publish offline yourself before disconnecting, as cleanShutdown() does.
Duplicate client IDs. If two devices use the same client ID, the broker disconnects the older connection when the newer one connects, which triggers the old one's will and creates confusing status changes. Derive the ID from the MAC address.
Deep Sleep Devices
Battery nodes that wake, publish and sleep are a special case. If they disconnect cleanly before sleeping, the will never fires, and a status topic would flip online/offline with every cycle. A better approach for sleepy devices is to publish a retained last_seen timestamp or uptime counter, and let the consumer decide a device is offline when that value is older than a few intervals. The ESP32 battery powered sensor guide discusses this trade-off.
MQTT 5 Will Delay and Broker-Side Status
PubSubClient speaks MQTT 3.1.1. With an MQTT 5 client such as ESP-IDF's esp-mqtt, you can also set a will delay interval, so the broker waits a few seconds before publishing the will. If the device reconnects in that window, for example after a brief Wi-Fi hiccup, the will is cancelled and the status does not flap. The ESP-IDF MQTT example shows where the will is configured, and MQTT 5 features explains the property.
On the broker side, ConnectMQ also publishes connection state for each client under $SYS/broker/connection/{clientId}/state, which is useful as a cross-check when you debug an unexpected offline.
Show Device Availability on ConnectMQ
Everything above runs on the public MQTT test broker, so you can test failure modes without an account. For a real fleet, a private hosted MQTT broker keeps status topics away from public view. ConnectMQ's live topic explorer shows the retained status of every device at a glance. Create a broker on the free Serverless plan at app.connectmq.com, no credit card required, and connect with your generated YOUR_CLIENT_ID, YOUR_USERNAME and YOUR_PASSWORD using the connect() overload with credentials. For the complete device project, see the ESP32 IoT tutorial.
Frequently Asked Questions
When does the broker publish the Last Will?
When the client disappears without sending DISCONNECT: power loss, crash, network failure or a keepalive timeout. A clean disconnect() cancels the will.
Should the Last Will message be retained?
Yes, for status topics. Retaining it means any subscriber that connects later still sees that the device is offline.
Which QoS should I use for the will?
QoS 1 is a good default, as it ensures delivery to connected subscribers. The free ConnectMQ Serverless plan supports QoS 0 and 1, which covers this pattern.
Why does my device status flicker between online and offline?
Usually mqtt.loop() is blocked longer than the keepalive, or two devices share a client ID. Remove long blocking calls and give every device a unique ID.



