Skip to content

feat: opt-in ASYNCWEBSERVER_USE_PSRAM - allocate handler objects in PSRAM (ESP32) - #468

Open
inF1704 wants to merge 1 commit into
ESP32Async:mainfrom
inF1704:feat/psram-handlers
Open

feat: opt-in ASYNCWEBSERVER_USE_PSRAM - allocate handler objects in PSRAM (ESP32)#468
inF1704 wants to merge 1 commit into
ESP32Async:mainfrom
inF1704:feat/psram-handlers

Conversation

@inF1704

@inF1704 inF1704 commented Jul 28, 2026

Copy link
Copy Markdown

On ESP32 targets with PSRAM, internal SRAM is often the scarcest resource (lwIP segments, WiFi buffers, WS message queues all live there). Every AsyncWebHandler-derived object (route handlers, catch-all, AsyncWebSocket, static file handlers) currently comes from the internal heap, though handlers are created once after PSRAM init and only read during request dispatch — never from ISR/DMA context, and latency is irrelevant for them.

This PR adds an opt-in build flag -D ASYNCWEBSERVER_USE_PSRAM that routes handler allocation through heap_caps_malloc(MALLOC_CAP_SPIRAM) with a transparent fallback to the internal heap, so it is safe even on targets without PSRAM. Without the flag, behavior is bit-identical to today — no risk for existing users.

Context: we run a dashboard-heavy ESP32-S3 firmware (same field setup that produced #453) where internal SRAM exhaustion cascades into WiFi/ESP-NOW allocation failures. Moving handlers out of internal RAM is one of the mitigations we have been running in production since mid-July.

@mathieucarbou mathieucarbou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will let @me-no-dev and @willmmiles also review.

I am not against the idea of allowing an easy usage of PSRAM for the library especially for objects that are a long lived. This would probably be a good and easy way to free some memory easily for users running with PSRAM.

But I am also wondering why you picked only this place ? For example, middleware could benefit of that too, and a few others little classes also. If we accept the idea, I would probably like to apply this pattern more globally across the project where it can be applied.

Comment thread src/ESPAsyncWebServer.h
if (!p) {
p = malloc(size);
}
return p;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if new fails on ESP32 it does throw std::bad_alloc (which crashes the esp). Here it would return null right ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, operator new(size_t) is required to throw std::bad_alloc on an allocation failure. operator new(size_t, const std::nothrow&) is the variant that returns null.

@willmmiles

Copy link
Copy Markdown

But I am also wondering why you picked only this place ? For example, middleware could benefit of that too, and a few others little classes also. If we accept the idea, I would probably like to apply this pattern more globally across the project where it can be applied.

^^^ This. Why just the handler objects? Why not the request objects? The middleware? Response objects? The buffers used by the response objects (usually much bigger than the requests, and probably the largest allocations in the normal path)? The parsed header list entries? ..and so on.

I'm not opposed to considering support for custom allocators, but I don't think it makes sense to apply it ad-hoc on only one object type/family. I'd like to see an interface that's scoped over the whole library, with careful selection of which objects and where it's applied and some discussion as to which objects and data structures were selected or not selected, and why.

As a second concern, any alternative allocation API should delegate the implementation details to the library user, rather than binding a single specific allocation flavour. Supporting only heap_caps_malloc(n, MALLOC_CAP_SPIRAM) vs something like heap_caps_malloc_prefer(n, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT) is the kind of problem where I do not want to be dealing with PRs to add every application's preferences. The better approach is to define an API with common definitions that can be called upon in every usage site, and allows library users to override the built-ins with their own favourite approach (similar conceptually to AsyncWebServerLogging.h's AsyncWebServerLoggingCustom.h). We would want to take care to avoid overriding operator new unless a custom allocator was actually in use.

@inF1704

inF1704 commented Jul 28, 2026

Copy link
Copy Markdown
Author

Fair points on both counts, thanks!

On the operator new semantics: agreed... this should either be the std::nothrow overload pair, or the throwing variant with a proper std::bad_alloc. In our production firmware the null case only occurs when both heaps are exhausted (the device is beyond recovery at that point anyway), but that's no excuse for non-conforming semantics in a library.

On scope: I deliberately kept the PR minimal to gauge interest, but I fully support going broader. A pluggable allocator interface (as @willmmiles suggests, similar to what ArduinoJson does) seems like the most future-proof design. I'm happy to rework the PR in that direction if you can settle the preferred shape (static allocator hooks vs. an allocator object), or equally happy if you'd rather drive the design yourselves.

@mathieucarbou

Copy link
Copy Markdown
Member

Why just the handler objects? Why not the request objects? The middleware? Response objects? The buffers used by the response objects (usually much bigger than the requests, and probably the largest allocations in the normal path)? The parsed header list entries? ..and so on.

I like the interface idea, especially because we do not have to deal with the many specific user implémentations.

But how to decide when the allocator would be used ? Using PSRAM is slower and psram is best suited for large buffers like images. So one may want to not use psram for requests and responses because he has an app with a lot of short lived clients.

also, frequently accessed memory would be in the shared cache with ram: so how much do we gain in putting middleware’s for example, where global ones like security would be accessed on every requests ?

So even scoping the psram usage is complicated.

in the case of middlewares that is easy because they are created by the user (except if a function is passed). So there is already a way to have the user control how they are allocated.

but there could be some situations where this is more fuzzy. For example if someone setups a route for /* as a fallback route, or an error handler , this one probably should be on heap and not psram for speed concerns ?

So maybe we could first look at the scopes : what could be and cannot be in psram, and also what kind of allocations the use control.

for example, if the user had a way to add handlers to the server in the same way it is done for middleware, then the user could control itself allocation.

so maybe we first need also to look if there is a way to refactor the lib to allow the user control a bit more the allocation part per object ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants