A board that restarts on its own is not one problem, it is four, and they are easy to tell apart because each prints something different at the moment it happens. Open the serial monitor at 115200 and read the line printed immediately after the restart. Everything follows from that string.
The four signatures
| Serial output | Cause | Where to look |
|---|---|---|
Brownout detector was triggered | Supply voltage sagged | Power, cable, regulator, capacitors |
Task watchdog got triggered | A task blocked too long | The named task’s loop |
Guru Meditation Error … LoadProhibited | Code faulted | Null or uninitialised pointer |
rst:0x10 (RTCWDT_RTC_RESET) repeating | Fails before your code runs | Flash, partition table, bad image |
Brownout
This is power, not code, almost every time. The chip detected the supply falling below threshold and reset deliberately rather than behaving unpredictably.
It shows up the moment the radio transmits, because a Wi-Fi burst can draw several hundred milliamps in spikes against an idle draw of a few tens. Anything sized for the average will sag.
The usual culprits, in the order worth checking:
- A thin or long USB cable. Many are charge-oriented and drop meaningful voltage under load.
- A laptop port or hub that cannot supply the peak.
- Powering from a sensor board’s 3.3 V regulator rather than a supply sized for the module.
- No bulk capacitance near the module. A few hundred microfarads across the supply pins absorbs the spikes that the regulator cannot respond to quickly enough.
If it only browns out on battery, the battery’s internal resistance is the constraint, not capacity.
Task watchdog
A task held a core longer than the timeout without yielding. The message names the task, which is usually enough.
Typical causes are a long blocking loop with no yield, delay() used inside a task where
vTaskDelay() belongs, a slow flash or filesystem write, or a network call without a timeout. The
fix is to yield, or to move the work off the loop, rather than to raise the timeout — raising it
hides the symptom and the underlying stall remains.
Guru Meditation
The code faulted. LoadProhibited is the most common and means a null or uninitialised pointer was
dereferenced.
The backtrace is a list of addresses that mean nothing by themselves. Decode it with the ESP Exception Decoder against the exact ELF from the build that crashed — a rebuilt binary produces different addresses and a plausible but wrong answer, which is worse than none.
Bootloop before your code
If the reset repeats with rst:0x10 (RTCWDT_RTC_RESET) and you never see your own output, it is
failing before setup(). Usually a bad flash, a partition table that does not match the image, or
an image built for a different chip. Erase the flash entirely and reflash rather than flashing over
the top.
Seeing it on a board you cannot reach
The serial monitor works when the board is on your desk. It is no use when the device is in a greenhouse and restarting once a day.
The ESP-IDF system API exposes why the last reset happened, so a board can report its own cause on the way back up:
#include <Nodrix.h>
#include <esp_system.h>
const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-password";
const char* HOST = "nodrix.you.workers.dev";
const char* TOKEN = "tok_your_project_token";
const char* resetReason() {
switch (esp_reset_reason()) {
case ESP_RST_POWERON: return "poweron";
case ESP_RST_SW: return "software";
case ESP_RST_PANIC: return "panic"; // Guru Meditation
case ESP_RST_INT_WDT: return "int_wdt";
case ESP_RST_TASK_WDT: return "task_wdt";
case ESP_RST_BROWNOUT: return "brownout";
case ESP_RST_DEEPSLEEP:return "deepsleep";
default: return "other";
}
}
void setup() {
Serial.begin(115200);
Nodrix.setFirmwareVersion("1.0.0");
Nodrix.begin(WIFI_SSID, WIFI_PASS, HOST, TOKEN);
// Reported once per boot: why we restarted, and an event to mark the moment.
Nodrix.send("reset_reason", resetReason());
Nodrix.event("device_boot");
}
void loop() {
Nodrix.run();
static uint32_t last = 0;
if (millis() - last > 60000) {
last = millis();
Nodrix.send("uptime_s", (long)(millis() / 1000));
Nodrix.send("heap_free", (long)ESP.getFreeHeap());
}
}
Three variables turn a mystery into a diagnosis:
reset_reasondistinguishes the four cases above without a cable.brownoutpoints at power,panicat code,task_wdtat a blocking loop.uptime_sresets to zero on every restart. A sawtooth on the chart is a board rebooting, and the period tells you how often — a detail that is invisible if you only look at sensor values.heap_freetrending downward over hours is a memory leak, and the crash that eventually follows is a consequence rather than the problem. This is the one you cannot see any other way.
On the deployment, an automation on the device_boot event sends to Telegram, Discord, Slack or
email, so an unexpected restart arrives as a message rather than as a gap you notice next week. A
board that reboots nightly at 03:00 tells you something quite specific about your power situation,
but only if someone is counting.
Order of attack
- Read the serial line. It identifies which of the four you have and saves hours of guessing.
- Rule out power first. It is the cheapest to test — a better cable and a proper supply — and it is the most common cause on a board that worked on the bench and fails in place.
- Decode before theorising. A backtrace with the right ELF is precise; a guess about it is not.
- Watch
heap_freeif it takes hours to fail. Slow failures are usually leaks, and no amount of staring at the crash point reveals that.