Configuration¶
Important recommendations for build options¶
Most of the crashes are caused by improper use or configuration of the AsyncTCP library used for the project. Here are some recommendations to avoid them and build-time flags you can change.
-
CONFIG_ASYNC_TCP_MAX_ACK_TIME: defines a timeout for TCP connection to be considered alive when waiting for data. In some bad network conditions you might consider increasing it. -
CONFIG_ASYNC_TCP_QUEUE_SIZE: defines the length of the queue for events related to connections handling. Both the server and AsyncTCP library were optimized to control the queue automatically. Do NOT try blindly increasing the queue size, it does not help you in a way you might think it is. If you receive debug messages about queue throttling, try to optimize your server callbacks code to execute as fast as possible. -
CONFIG_ASYNC_TCP_RUNNING_CORE: CPU core thread affinity that runs the queue events handling and executes server callbacks. Default is ANY core, so it means that for dualcore SoCs both cores could handle server activities. If your server's code is too heavy and unoptimized or you see that sometimes server might affect other network activities, you might consider to bind it to the same core that runs Arduino code (1) to minimize affect on radio part. Otherwise you can leave the default to let RTOS decide where to run the thread based on priority -
CONFIG_ASYNC_TCP_STACK_SIZE: stack size for the thread that runs sever events and callbacks. Default is 16k that is a way too much waste for well-defined short async code or simple static file handling. You might want to cosider reducing it to 4-8k to same RAM usage. If you do not know what this is or not sure about your callback code demands - leave it as default, should be enough even for very hungry callbacks in most cases. -
ASYNCWEBSERVER_USE_CHUNK_INFLIGHT: inflight control for chunked responses. If you need to serve chunk requests with a really low buffer (which should be avoided), you can set-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=0to disable the in-flight control. -
ASYNCWEBSERVER_DIGEST_NONCE_TTL: validity window, in seconds, of a Digest authentication nonce (default: 60). Digest replay protection uses stateless signed nonces: beyond this window a nonce is stale and clients silently re-authenticate via thestale=truechallenge directive. Set to 0 to disable expiry (not recommended: widens the replay window). Can also be tuned at runtime withserver.setDigestNonceTimeout().
[!NOTE] This relates to ESP32 only, ESP8266 uses different ESPAsyncTCP lib that does not has this build options
Recommended configurations:
-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000 // (keep default)
-D CONFIG_ASYNC_TCP_PRIORITY=10 // (keep default)
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 // (keep default)
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1 // force async_tcp task to be on same core as Arduino app (default is any core)
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096 // reduce the stack size (default is 16K)
Important things to remember¶
- This is fully asynchronous server and as such does not run on the loop thread.
- You can not use yield or delay or any function that uses them inside the callbacks
- The server is smart enough to know when to close the connection and free resources
- You can not send more than one response to a single request
Memory placement: PSRAM and static allocation¶
On ESP32 targets with PSRAM, internal SRAM is often the scarcest resource (lwIP segments, WiFi buffers, and other networking allocations all live there). Handler objects — AsyncCallbackWebHandler, AsyncStaticWebHandler, AsyncWebSocket, AsyncEventSource, AsyncCallbackJsonWebHandler — are long-lived: they are created once at setup and only read during request dispatch. Moving them out of internal SRAM can free significant memory.
By default, handlers created by server.on() or server.serveStatic() are allocated on the internal heap and owned by the server. To control where handler objects live, register them as non-owning:
[!WARNING] When borrowing a handler by reference, do not use a stack-allocated (automatic) object unless it outlives the server. In typical Arduino sketches, a local variable inside
setup()is destroyed whensetup()returns — always usestaticor global storage for borrowed handlers.
// Static RAM — no heap allocation at all
static AsyncCallbackWebHandler handler;
handler.setUri(AsyncURIMatcher::exact("/status"));
handler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
server.addHandler(handler); // non-owning reference overload
// PSRAM — placement new with heap_caps_malloc (ESP32)
#include <esp_heap_caps.h>
auto *handler = new (heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) AsyncCallbackWebHandler();
handler->setUri(AsyncURIMatcher::exact("/status"));
handler->onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
server.addHandler(*handler); // non-owning borrow — manage lifetime yourself
The same applies to the catch-all (not-found) handler — either at construction time or afterwards:
// At construction — non-owning borrow, lives in static RAM
static AsyncCallbackWebHandler myCatchAll;
AsyncWebServer server(80, myCatchAll);
// At construction — non-owning borrow from PSRAM
AsyncWebServer server(80, *psramCatchAll);
// After construction
server.setCatchAllHandler(myCatchAllHandler); // non-owning borrow
server.setCatchAllHandler(*psramCatchAll); // non-owning borrow from PSRAM
server.setCatchAllHandler(new AsyncCallbackWebHandler()); // server-owned
[!NOTE] When using non-owning registration, you are responsible for keeping the handler alive while it is registered with the server, and for destroying it after unregistration. For PSRAM handlers, remember to call the destructor explicitly and then
heap_caps_free()the memory block.
See the CustomAllocation example and the Routing documentation for more details.