Why an ESP8266 Web Server Freezes When a Handler Calls delay()
You add a "pulse the relay for five seconds" button to your ESP8266 web page, and while the relay is on, the whole page goes dead. Other tabs hang, the status endpoint times out, and then everything comes back at once. The chip hasn't crashed and WiFi hasn't dropped. Your handler is simply sitting in the only place requests can be served from.
The code that causes it
This is the typical shape, using the stock ESP8266WebServer from the Arduino core:
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
const int RELAY = D1;
void handlePulse() {
digitalWrite(RELAY, HIGH);
delay(5000); // the problem
digitalWrite(RELAY, LOW);
server.send(200, "text/plain", "done");
}
void handleStatus() {
server.send(200, "text/plain", "ok");
}
void setup() {
pinMode(RELAY, OUTPUT);
// WiFi.begin(...) and connection wait omitted
server.on("/pulse", handlePulse);
server.on("/status", handleStatus);
server.begin();
}
void loop() {
server.handleClient();
}
Why the server stops, but the chip doesn't
The server has no threads. handleClient() checks for a waiting connection, reads the request, calls your handler, and only returns when the handler does. While your handler runs, nothing else calls handleClient(), so nobody else gets served.
Meanwhile, delay() on the ESP8266 is not a pure busy-wait. It hands control back to the SDK, so the WiFi stack keeps running and the watchdog stays fed. That is why the board looks healthy: it answers pings, keeps its association, and shows no reset. It is just not accepting new work.
Incoming connections are still completed by the TCP stack underneath, so a second request isn't refused. It waits in the queue until the handler finishes. That explains the "everything arrives at once" behaviour, and why the delay feels longer than five seconds if you have several clients retrying.
Quick detour: why browsers make it look worse
A browser rarely sends one request. Load the page and it often follows up with a /favicon.ico request, and a page that polls /status every second stacks up requests behind the blocked one. Each queued request then runs in turn after your handler returns, so the freeze looks like several separate stalls. Testing with curl shows the cleaner picture: one blocked call, one queue.
Fix: record the deadline, return immediately
Don't wait inside the handler. Switch the relay on, note when it should switch off, send the response, and let loop() do the rest:
unsigned long relayOffAt = 0;
bool relayOn = false;
void handlePulse() {
digitalWrite(RELAY, HIGH);
relayOn = true;
relayOffAt = millis() + 5000;
server.send(200, "text/plain", "started");
}
void loop() {
server.handleClient();
if (relayOn && (long)(millis() - relayOffAt) >= 0) {
digitalWrite(RELAY, LOW);
relayOn = false;
}
}
The subtraction cast to long is deliberate. millis() wraps after roughly 49 days, and comparing with millis() >= relayOffAt breaks at the wrap. Subtracting first and treating the result as signed survives it.
Things to check after the change
- Hit
/pulse, then/statusstraight away from another terminal. Status should answer instantly. - Call
/pulsetwice in a row. Decide whether the second call extends the timer or is ignored; the code above restarts it. - Make sure nothing else in
loop()blocks, such as a sensor read with a long wait, or you have just moved the problem.
The async library has a nastier version
ESPAsyncWebServer looks like it avoids all this, since requests are handled from callbacks instead of a polled loop. It does, but those callbacks run inside the TCP stack's context. Calling delay() there is documented as something to avoid, because blocking in that context can starve the network stack and trip the watchdog, giving you a reset instead of a stall. The fix is the same: set a flag or deadline and finish the work from loop() or a ticker.
The wider rule on a single-core cooperative system is that every function you write borrows the CPU from everything else. A handler should do its small piece of work and give the CPU back; anything that takes time becomes state plus a deadline.