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)

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)

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)

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


A other way to save power is to use a interrupt driven system: Setup a "handle_packet" (software?) interrupt what calls that handleClient function, this assures also a very fast responce.

BTW. I'm *not* familiar with the ESP but have worked about 30 years as a realtime programmer on the 8051 for fruitgrading manufactor. All hardware and software where done inhouse.

Great find thanks for sharing. This is something I need for a project right now!

Tried and true

On STM32 and therefore I assume other ARM-core processors there's a Wait For Interrupt (WFI) assembly instruction that may be "better" than a simple delay if you have any sort of interrupts firing - in my code there's almost always a 1ms SysTick interrupt, but also things like an I2C or UART interrupt are common.

Posted by JohnU

Nice info.

You may be able to hack the main function code in the file core_esp8266_main.cpp. I remove the Serial check and update for times I don't need it.

Posted by oswald

Hi it seems that the delay will allow the esp8266 to go into modem-sleep.
While in Modem-sleep, the ESP8266 will disable the modem (WiFi) as much as possible. It turns off the modem between DTIM Beacon intervals. This interval is set by your router.
To optimise the sleep you can sync the esp8266 sleep/delay time with the DTIM Beacon interval of our router. This will guarantee response time and minimise power consumption.
DTIM Beacon interval basically sets how long the router will buffer the packet to deliver to the client when it can not be delivered immediately. There are some examples online that implement this in an more advanced manner, basically only going to sleep when no packets are awaiting delivery.

Posted by fred

I forgot to mention in the previous comment:

It also seems that the ESP32 while more powerful (and drawing more current when active) can use the DTIM Beacon sleep method more efficiently, basically sleeps deeper.
Therefore for applications that have a low uptime/high downtime (like a webserver) you may want to consider using the ESP32 since it can save some power in the long run.

The DTIM Beacon sleep maintains the session with the router therefore it takes less time/power to startup.

Posted by fred

Have you considered rewriting your code in the Rust programming language? Maybe then you could use Rust's excellent support for asynchronous programming, like futures and the async and await keywords?

Posted by Jen Thompson

Does server.handleRequest() tell you if it did any work or not? With a 1 ms delay per iteration, even with instantaneous processing, two ~simultaneous requests will incur latency from the 'artificial' delay. Better would be to call server.handleRequest() until it runs out of work to do, and then go to sleep before polling again, but doing that requires being able to tell when it does, indeed, run out of work.

Posted by sam

Just had a poke in the Arduino source and found server.client().status() == CLOSED as something useful to determine whether to sleep.

Add a new comment

(No HTML tags allowed. Separate paragraphs with a blank line.)