ESPHome's MQTT client can publish a shutdown_message when a device restarts, shuts down or goes into deep sleep, so anything subscribed to the topic knows the device left on purpose. In practice that message often never arrived. A fix by bdraco, merged into the development branch on 25 October 2026, makes the client wait until the broker confirms it. It should ship with ESPHome 2026.11 and needs no change to existing configurations, such as the example from the pull request:

mqtt:
  broker: 192.168.1.10
  shutdown_message:
    topic: livingroom/status
    payload: offline

Why the message got lost

The old code published the shutdown message and called disconnect straight away, with only a yield() in between. The MQTT client had no teardown step, so the reboot followed within microseconds and the packet often never left the device. The shutdown button and switch did not run any teardown at all.

bdraco's bench numbers show how bad it was. On the current development code the message arrived on 0 of 10 restarts on an ESP32, 3 of 10 on an ESP8266 and 0 of 3 ESP32 deep sleeps. With the fix, the same tests gave 10 of 10 restarts on both chips and 3 of 3 deep sleeps.

Why not just wait a moment

A first version of the fix held the disconnect for a fixed 100 ms. That got the message through on only 3 of 10 ESP32 restarts. A packet capture showed that on the failed attempts the ESP32 sent nothing at all for about 465 ms after the restart command, because of Wi-Fi stalls and TCP retransmissions. A 900 ms window worked 8 times out of 8, but it would have delayed every reboot that long.

What changed

The shutdown message is now sent at the QoS set for it, but at least QoS 1, so the broker has to acknowledge it. The disconnect moved into the component's teardown step, which waits for that acknowledgement. The wait usually lasts a few milliseconds. On a bad link it is capped by the core teardown timeout: 1 second for a reboot and 5 seconds before deep sleep.

A new App.safe_shutdown() function runs the shutdown hooks, the time-limited teardown and the power-down hooks in one go. safe_reboot(), deep_sleep and the shutdown button and switch now all use it, so the shutdown platforms also tear down cleanly before the chip sleeps. The mqtt.disable action still disconnects immediately.

The change covers the ESP-IDF MQTT backend and AsyncMqttClient, which ESPHome uses on the ESP8266 and LibreTiny chips. It costs 8 bytes of RAM on ESP-IDF and LibreTiny and 4 bytes on the ESP8266. If Home Assistant or another subscriber shows your MQTT devices as offline based on this topic, the status after a planned restart or deep sleep should now be reported reliably instead of depending on luck.