ESP32 and Arduino

ESP32 MQTT Last Will: Reliable Online/Offline Status for Every Device

Configure a retained Last Will and Testament plus a birth message on the ESP32 so every dashboard and script can see device availability instantly, including after crashes, power cuts and Wi-Fi loss.

Dashboard listing devices as online and offline using ESP32 MQTT Last Will messages
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

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:

  1. Normal start. Power the board. You see .../status online within a few seconds.
  2. 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.
  3. Clean shutdown. Power it back on, wait for online, then press BOOT. offline appears 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.

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.