ESP8266 web server is slow to close connections

14.08.2022 16:48

A side note to my previous post about using delays to save power on an ESP8266 web server. When I was using Apache Bench to measure the server response times, I noticed that the total time for a request to the ESP8266 always seemed to be about 2 seconds:

$ ab -n10 http://esp8266.lan/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>

...

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        2   16  36.6      4     120
Processing:  2006 2008   4.0   2007    2017
Waiting:        5    7   3.9      6      16
Total:       2009 2024  39.7   2010    2137

This looked odd. When opened in a browser, there was no noticeable 2 second delay and the page from the ESP8266 web server displayed immediately.

The way Apache Bench displays the Connection Times numbers is a bit odd to begin with. This post tells how to interpret the Connect, Processing and Waiting rows. In short, Processing is time from start to end of the TCP connection. Waiting is time from when the request was sent to when the first byte of the response was received.

Since Waiting time was short, this led me to believe that the whole response is sent quickly, but the connection is not closed until after a 2 second delay. A quick tcpdump confirmed this:

(client opens connection)
17:43:27.892032 IP client.lan:55422 > esp8266.lan:80: Flags [S], seq 3077448494, win 64240, options [mss 1460,sackOK,TS val 3187045141 ecr 0,nop,wscale 7], length 0
17:43:27.923811 IP esp8266.lan:80 > client.lan:55422: Flags [S.], seq 6555, ack 3077448495, win 2144, options [mss 536], length 0
17:43:27.923855 IP client.lan:55422 > esp8266.lan:80: Flags [.], ack 1, win 64240, length 0
(client sends request)
17:43:27.923968 IP client.lan:55422 > esp8266.lan:80: Flags [P.], seq 1:83, ack 1, win 64240, length 82: HTTP: GET / HTTP/1.0
(esp8266 sends response)
17:43:27.929828 IP esp8266.lan:80 > client.lan:55422: Flags [P.], seq 1:85, ack 83, win 2062, length 84: HTTP: HTTP/1.0 200 OK
17:43:27.929861 IP client.lan:55422 > esp8266.lan:80: Flags [.], ack 85, win 64156, length 0
17:43:27.931314 IP esp8266.lan:80 > client.lan:55422: Flags [P.], seq 85:97, ack 83, win 2062, length 12: HTTP
17:43:27.931337 IP client.lan:55422 > esp8266.lan:80: Flags [.], ack 97, win 64144, length 0
(esp8266 closes connection - note 2 s delay in timestamps)
17:43:29.934077 IP esp8266.lan:80 > client.lan:55422: Flags [F.], seq 97, ack 83, win 2062, length 0
17:43:29.934227 IP client.lan:55422 > esp8266.lan:80: Flags [F.], seq 83, ack 98, win 64143, length 0
17:43:29.936098 IP esp8266.lan:80 > client.lan:55422: Flags [.], ack 84, win 2061, length 0

In HTTP/1.0, the server should close the connection as soon as the complete response has been sent. It seems that ESP8266WebServer does this differently for some reason. Instead of the server closing the connection, it waits for the client to close it. There's also a timeout that closes the connection from the server side if it's not closed by the client. The timeout is controlled by the HTTP_MAX_CLOSE_WAIT constant, which is set to 2 seconds.

This is weird, because ESP8266 even sends a Connection: close header in the response, explicitly signaling to the client that the server will close the connection.

I think this is due to a benign bug in the ESP8266WebServer's state machine. After spending some time looking at the source, it seems like the current code intends to properly implement both keep-alive and non-keep-alive connections, but for some reason still uses the client timeout in the later case.

I wanted to dig a bit deeper into this and perhaps send a pull request, but instead I spent a bunch of time trying to get the current git version of ESP8266 core for Arduino to work with my ESP8266 modules. This turned out to be a whole new can of worms.

Posted by Tomaž | Categories: Code | Comments »

Saving power on an ESP8266 web server using delays

13.08.2022 17:19

ESP8266 core support on Arduino comes with a library that allows you to quickly setup and run a web server from your device. Commonly the code using the ESP8266WebServer library looks something like the following:

#include <ESP8266WebServer.h>

ESP8266WebServer server(80);

void setup(void)
{
	... various wi-fi and request handler setup calls here ...
}

void loop(void)
{
	server.handleClient();
}

For a more concrete example, take a look at the "Hello World" example that is included with the library.

In the following we'll focus on the loop() and ignore the rest. As the name implies, the Arduino framework calls the loop() function in an infinite loop. The handleClient() method checks if any client has connected over the Wi-Fi network and issued an HTTP request. If so, it handles the request and sends a response back to the client. If not, it exits immediately, doing nothing.

In other words, loop() implements a busy-wait for HTTP requests. Even when there is nothing to do, the CPU still runs the busy loop roughly 100000 times per second. In the common case where a page is only requested from the server every once in a while, the system will spend almost all of its time in this state.

Busy loops are common on microcontrollers. On an ATmega microcontroller, another popular target for Arduino code, this is hardly a problem. Unless you're running your hardware off a battery and really counting every microwatt, a busy CPU uses negligible extra power over a CPU that's sleeping. The ESP8266 is a bit more powerful than an 8-bit microcontroller though and correspondingly has a higher power consumption when not sleeping. Hence it makes sense to be a bit more sensible about when the CPU is running or not.

The best solution would be to not have a busy loop at all. Ideally we could use the underlying RTOS to only schedule a handleClient() task in response to network events. The RTOS is smart enough to put the CPU to sleep when no task needs to run. Unfortunately, the simple Arduino environment does not want us to mess with the RTOS. The ESP8266WebServer library certainly isn't written with such use in mind. This approach would require a lot of refactoring of ESP8266WebServer and the code that uses it.

A simpler way is to slow the busy loop down. There is no need to check for client connections hundreds of thousand times per second. A more modest rate would be good enough and the CPU can sleep in between. This would decrease the power consumption at the cost of the response time for HTTP requests. Inserting the Arduino-provided delay() function into loop() does exactly what we want:

void loop(void)
{
	server.handleClient();
	delay(...);
}

The question that remains is what is a good value to choose for delay in this case. What value gives the best trade off between decreased power consumption and increased response time?

My setup for measuring ESP8266 power consumption.

To check I've used this somewhat messy breadboard setup. I've measured the power consumption and response times when running a simple web server Arduino sketch, similar to the "Hello World" included with the ESP8266WebServer library, but with various delays inserted into the loop.

A multimeter with an averaging function measured the current on the 3.3 V supply line of an ESP-01 module. Averaging time was 30 s. I've measured the power consumption while the module was idle (not answering any HTTP requests)

The module was connected through an USB development board to a PC for easy programming from the Arduino IDE. I've used Arduino 2.8.19 and the ESP8266 core 2.4.2.

I've measured HTTP server response times over the Wi-Fi network using Apache Bench (ab -n10). As the representative metric I've chosen the median time from the Waiting row. This is the time it took ESP8266 to send the first byte of the response.

Supply current versus various delays in loop()

Response time versus various delays in loop()

Here are the results, comparing code with just a call to handleClient() to code that is also calling delay with various argument values. The argument to delay is wait time in milliseconds. I've also did a measurement with yield() instead of delay(), something that I've seen used in some examples.

Even a 1 millisecond delay, the minimum allowed by the function, decreases the idle power consumption by 60%. Not surprising if you consider that this small delay causes the CPU to be idle 99.999% of time. The added delay does also measurably increase the response time, as was expected, but the difference between 6 or 8 ms should be negligible for most practical use cases.

Further increasing the delay does not bring any benefits. It only increases the response time without measurably decreasing power consumption. I suspect the variations in measured current with higher delay values are likely due to uncontrolled traffic on the network waking up the ESP8266's radio more often in some test runs than in others.

Adding a yield() call to loop() does not show any benefits in this test. In fact, handleClient() itself calls yield() in some cases before returning. Similarly, adding delay(0) has no measurable effect.

In conclusion, I recommend using a loop() with 1 ms delay:

void loop(void)
{
	server.handleClient();
	delay(1);
}

The added delay decreases the idle power consumption at 3.3 V by about 60%, from around 230 mW to around 70 mW. This is significant enough that you can feel the board running cooler by touch. On a yearly basis it saves around 1.4 kWh per device that's powered on continuously.

For most simple Arduino sketches using ESP8266WebServer, like using the request handler to read a sensor or actuate a relay, this is just a simple one-line change, so I think the power saving is worth the effort. Of course, if you're doing something else in loop() in addition to just calling handleClient(), adding a delay might have other side effects. In such case the code running in loop() might need some adjusting to account for the delay.

Posted by Tomaž | Categories: Digital | Comments »