Repository navigation
Lost writes using AsyncCallbackResponse during low memory #242
Description
Activity
- changed the title
[-]Lost writes using AsyncCallbackResponse[/-][+]Lost writes using AsyncCallbackResponse during low memory[/+]on Jul 28, 2025 Good point.
The entire library (also at other places) is made in a way to degrading itself to avoid crashing the ESP as much as possible
So this is one of the thing that can happen yes.
Under memory pressure, there will be other places were espasync will degrade itself, like during request parsing for example.This behavior was chosen by the original library author @me-no-dev.
For that specific point, yes if you could build a MRE / Example and put it in the example folder and send a PR. That will be a good use case to start a discussion also.
CC @willmmiles
CC @willmmiles
@mathieucarbou You pegged me all right: I did indeed solve this one in the WLED fork. Graceful handling of low memory conditions was the overarching goal behind the changes in that project. You can build a test case by consuming internal DRAM such that
client()->space() > heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DEFAULT)- essentially arranging that there isn't enough heap available to allocate the full 2*MSS that the TCP connection could permit.The solution there ended up being fairly complex:
- First, keep the output buffer around if it doesn't send completely; track how much of it is left
- Second, if there's leftover content in the output buffer at the start of an
_ackcall, send it first before considering anything else - Third, double-check that there's enough space for the LwIP buffers when allocating the output buffer -- because there's no point in
AsyncAbstractResponseallocating a large output buffer if the TCP stack can't then allocate apbufto send.
I did not opt to shrink the buffer to remove the successfully written data, though this could have been implemented as another solution for reducing ongoing memory load.
Unfortunately I'm a bit oversubscribed at the moment and I won't be in a position to PR these changes upstream anytime in the near future (sept-oct time frame maybe). If anyone else wants to take a stab at it, one key thing to be aware of there is the use of
class DynamicBuffer, which is essentially a shallow replacement forstd::vector<char,default_init_allocator>that permits allocation failure without exceptions. (Our project requires graceful handling of memory allocation failures on ESP8266, which doesn't support C++ exceptions, sostdcontainers cannot be used.default_init_allocatoris a construct that allows a vector to beresize()d without zero-initializing the contents, so it's slightly faster when used for transient buffers.)Also: I believe there's a parallel issue with incomplete writes in the websockets layer should you find yourself in a condition where
client()->space() > heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DEFAULT), so a message can be only partially sent. I left a TODO there but never implemented a solution.Thanks @willmmiles !
I was pretty sure you got it handled ;-)- addedType: QuestionFurther information is requestedFurther information is requestedand removed
on Sep 6, 2025 This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.
This issue was closed because it has been stalled for 7 days with no activity.
- linked a pull request that will close this issuefix: AsyncAbstractResponse might loose part of send buffer #316
on Oct 19, 2025 @jonny5532 : would you be able to provide a MRE or point to this branch's PR in order to test this fix done by @vortigont ?
Thank you!
- addedType: BugSomething isn't workingSomething isn't workingand removedType: QuestionFurther information is requestedFurther information is requested
on Oct 19, 2025 @jonny5532 @vortigont : FYI I added an example in the project to show how to send large responses:
Sadly I am not able to reproduce the problem through these examples and with the code in main.
Is one of you capable of that ? If yes, could you please share your MRE ?
Platform
ESP32
IDE / Tooling
Arduino (IDE/CLI)
What happened?
When sending a 16kb response in three parts, using
AsyncCallbackResponse, the middle one sometimes goes missing (so the received data is ~5k shorter than expected - the page finishes but with ~5k fewer bytes than theContent-Lengthindicated).This seems to be due to this:
ESPAsyncWebServer/src/WebResponses.cpp
Line 476 in 80af245
where the code assumes that the whole buffer got written (it
frees it after), even if it was only partially sent. Ideally it could signal to theAsyncCallbackResponsethat it needs to rewind, but being in the Abstract superclass it can't access_filledLengthto decrement it.AsyncTCP's
write(...)allocates memory by default, and this system is very memory constrained, so that is probably why it is not consuming the whole buffer (probably not any of it). It looks like the code above does checkspace()though, but that doesn't take into account the free memory that thewrite()might need to allocate.Maybe this is a situation you don't want to handle - as there's no guarantee there'll ever be enough free RAM? In practice it works if I rewind (eg. by adding a
rewindmethod which theAsyncCallbackResponsecan implement to roll back_filledLength).Thanks!
Stack Trace
N/A
Minimal Reproductible Example (MRE)
This is the essence of it, but could build a full MRE if needed?
I confirm that: