Blog / Embedded

  • esp8266
  • arduino
  • watchdog
  • wifi
  • embedded
  • debugging

delay() Doesn't Reset Your ESP8266, the Loop Without It Does

The folk advice says delay() breaks WiFi on an ESP8266. In the Arduino core it is nearly the opposite: delay() is what keeps the chip alive, and the resets come from loops that never give the WiFi stack a turn. There is one real exception, which is calling delay() from the wrong place. Both cases are below.

The chip is doing two jobs on one core

The ESP8266 has a single CPU core. Your sketch and the vendor SDK (WiFi, TCP/IP, timers) take turns on it, cooperatively. Nothing pre-empts your code. If loop() runs for too long, the SDK never gets scheduled.

Two watchdogs notice this:

  • A software watchdog, which fires after roughly three seconds and prints Soft WDT reset plus a stack dump on the serial port.
  • A hardware watchdog behind it, which fires after a few more seconds and resets the chip with no message at all.

Both get fed when control returns to the SDK. The exact timings vary a little with core and SDK version, so treat "a few seconds" as the rule.

What delay() actually does

In the Arduino core for ESP8266, delay(ms) does not spin. It hands control to the SDK, lets it do its work, and comes back when the time is up. That includes feeding the watchdog. So this is perfectly safe:

void loop() {
  sendReading();
  delay(10000);   // ten seconds, no reset
}

Ten seconds is far longer than the watchdog limit, and nothing happens. The core also calls yield() for you at the end of each pass through loop(). Same idea: the SDK gets its turn.

The loops that really do reset it

The classic one is waiting for WiFi with an empty body:

WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
  // spinning, SDK never runs, connection never completes
}

This is a nasty one because the thing you are waiting for needs the SDK to run, and the SDK cannot run while you spin. It deadlocks until the watchdog ends it. Add delay(100); (or yield();) inside the loop and it works.

Other ways to starve it:

  • A while loop polling a flag that only a WiFi callback can set.
  • A long for loop doing heavy work (big JSON parse, crypto, a slow display refresh) without any yield.
  • Reading a socket in a tight loop with no yield() between attempts.
  • A very long delayMicroseconds(). That one is a real busy-wait and does not yield.

Quick detour: why is delayMicroseconds different?

Hang on, why would the two delays behave differently? Because microsecond delays need precision, and handing control to the SDK could hand it back late by milliseconds. So the core busy-waits for short intervals, and it is your job to keep them short. Tens of microseconds is fine; asking for a hundred milliseconds that way is not.

The exception: delay() in the wrong context

Here is where delay() really does cause a crash. Yielding only makes sense from your sketch's own context. Some callbacks run inside the SDK's own timer or event context, and calling delay() or yield() there is not allowed.

Typical offenders:

  • A Ticker callback (it runs in the SDK timer context, unlike Ticker's scheduled variant).
  • Interrupt handlers attached with attachInterrupt().
  • Some low-level callbacks such as ESP-NOW receive handlers.

The symptom is a crash with a stack dump rather than a quiet watchdog reset. The fix is to keep the callback tiny: set a flag or copy a value, return, and do the slow work in loop().

volatile bool due = false;
Ticker t;

void setup() {
  t.attach(5, []() { due = true; });   // no delay(), no network calls here
}

void loop() {
  if (due) {
    due = false;
    sendReading();   // slow work happens in loop context
  }
}

The core also offers Ticker::attach_scheduled() and schedule_function(), which run your code from loop() context instead. Check your core version's documentation for the exact names before relying on them.

How to tell which case you have

  1. Open the serial monitor at 115200 baud and watch the reset message.
  2. If you see Soft WDT reset and a stack, something ran too long. Look at the call addresses in the dump (the ESP exception decoder plugin will turn them into function names).
  3. If the chip reboots silently, suspect the hardware watchdog: a loop that also blocks the soft one, or interrupts disabled for too long.
  4. If you get an exception with a stack pointing at a callback, you are yielding from the wrong context.

Then apply the matching fix: add a yield to the long loop, or shrink the callback to a flag.

Long work that has no natural pause

Sometimes the work truly is long, such as parsing a big payload. Sprinkle yield(); every so often, or call delay(0);, inside the loop. Once per few milliseconds of work is plenty. Better still, split the job into slices and do one per loop() pass, so WiFi stays responsive too.

Turning the watchdog off is possible on this chip, but it is rarely the right answer. The watchdog is telling you the SDK has been starved, and a starved SDK will also drop your WiFi connection long before it resets anything.