July 6, 2026 by Alessandro Colucci
If you searched for this, you've probably already seen one of these in your serial monitor:
E (5231) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
E (5231) task_wdt: - loopTask (CPU 1)
E (5231) task_wdt: Tasks currently running:
E (5231) task_wdt: CPU 0: IDLE0
E (5231) task_wdt: CPU 1: loopTask
E (5231) task_wdt: Aborting.
Or worse: no error at all, just a silent reset in the field, reported by a customer who says the device "randomly restarts" every few hours. This article covers both — how the watchdog actually works on ESP32, the three mistakes that cause most unwanted resets, and what a production firmware should actually do when it fires.
People say "the watchdog" like it's one thing. On ESP32 there are three, and mixing them up is the first source of confusion:
loopTask too) and expects each to "check in" periodically. This is the one behind the error above.portDISABLE_INTERRUPTS() section, or a task hogging a core without yielding). It fires with a "Guru Meditation Error: IWDT" panic, not the task_wdt message above.For 90% of the "my ESP32 keeps resetting" cases in production firmware, you're dealing with the Task Watchdog — that's the focus of the rest of this article.
In ESP-IDF, the task watchdog is configured via esp_task_wdt_config_t and initialized once:
#include "esp_task_wdt.h"
esp_task_wdt_config_t twdt_config = {
.timeout_ms = 5000,
.idle_core_mask = (1 << 0) | (1 << 1), // watch both idle tasks
.trigger_panic = true,
};
esp_task_wdt_init(&twdt_config);
esp_task_wdt_add(NULL); // subscribe the current task
Every task you subscribe with esp_task_wdt_add() must call esp_task_wdt_reset() before the timeout elapses, or it gets flagged. On the Arduino-ESP32 core, the loop task is subscribed automatically with a default timeout (historically 5s, but check your core version — this changed across releases), which is why plain Arduino sketches rarely see this error until they add something blocking.
This is the single most common cause. Any of these can block long enough to starve the watchdog:
delay() or blocking sensor read longer than the timeout, inside a task that's subscribed to the watchdogwhile(1) polling loop waiting on a condition that never yields to the schedulerThe fix isn't always "feed it more often" — often the real fix is moving the blocking work off the watched task entirely (a dedicated FreeRTOS task not subscribed to the watchdog, or an async/callback-based API instead of a blocking one).
If you manually configure idle_core_mask and only watch core 0 while your blocking code runs on core 1 (or vice versa, depending on how you pinned tasks with xTaskCreatePinnedToCore), the watchdog simply never sees the problem — until it does, in a different, harder-to-reproduce way. Match your watchdog configuration to your actual task-to-core assignment, not to a copy-pasted default.
OTA writes, large NVS commits, and TLS handshakes are all legitimately slow — sometimes slower than a conservative default timeout. Rather than disabling the watchdog for these (which defeats its purpose), the usual production pattern is to call esp_task_wdt_reset() at safe checkpoints inside the slow operation itself, or to move it to an unwatched task if it's rare and well-isolated.
Preventing resets is only half the job — a device that ships to a customer needs to survive one gracefully and tell you why. On boot, check the reset reason:
#include "esp_system.h"
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_TASK_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_WDT) {
// Log it, to Serial, to NVS for later retrieval, or straight to your
// backend if you have connectivity.
}
And make sure a repeated watchdog trigger can't turn into an infinite reset loop — if your device watchdog-resets, reconnects to WiFi, and immediately re-triggers the same blocking call that caused the first reset, you've built a brick that resets every 5 seconds forever. A simple guard (count resets in a short window via RTC memory, fall back to a safe/degraded mode after N) avoids this.
idle_core_mask matches your real task-to-core pinningNone of this is exotic — it's the kind of boilerplate every production ESP32 firmware ends up needing, and every project reinvents it slightly differently. If you'd rather specify the behavior and get working code instead of wiring this up by hand again, that's exactly what PleaseDontCode is for.