-
-
Notifications
You must be signed in to change notification settings - Fork 34.8k
Description
Version
v25.6.1
Platform
Darwin XXX 25.3.0 Darwin Kernel Version 25.3.0: Wed Jan 28 20:48:41 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6041 arm64
Subsystem
http
What steps will reproduce the bug?
When an HTTP request is executed, data in AsyncLocalStorage isn't freed. To reproduce it:
import hooks from 'node:async_hooks';
import http from 'node:http';
import net from 'node:net';
import crypto from 'node:crypto';
// http.setMaxIdleHTTPParsers(1);
const store = new hooks.AsyncLocalStorage();
function sendRequest() {
store.enterWith({body: crypto.randomBytes(512)});
const proxyReq = http.request({
agent: null,
createConnection: net.createConnection,
method: 'HEAD',
hostname: '127.0.0.1',
headers: {
'connection': 'close'
},
port: 9009,
path: '/',
});
proxyReq.end();
}
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 100; j++) {
sendRequest();
}
}
setTimeout(() => {
console.log('done');
}, 1000 * 60 * 60);
When running this with node --inspect, one can see that there are 1000 AsyncContextFrame entries in memory, as well as the buffer stored in the AsyncLocalStorage. This happens with AsyncContextFrame enabled (Node 24+)
I stumbled upon this when making an outbound request from an HTTP server handler (req, res) => {...} where I was using AsyncLocalStorage to keep some request context. I noticed that the memory was freed if I made the outbound requests using fetch() instead of http.request.
Also, if http.setMaxIdleHTTPParsers(1); is uncommented, so that HTTP parsers aren't reused, the memory is also freed.
How often does it reproduce? Is there a required condition?
Always
What is the expected behavior? Why is that the expected behavior?
Whatever is stored in an AsyncLocalStorage should be freed when it goes out of scope.
What do you see instead?
It can be seen here that there are 1000 instances of AsyncContextFrame
Additional information
I downloaded Node's source and tinkered with the parser initialization in _http_client.js and with the AsyncWrap logic in node_http_parser.cc, but couldn't fix it. I see that freeParser() should free everything, but it seems something is being held. I'm not familiar with the relationship between AsyncWrap and V8-based new AsyncLocalStorage implementation.
I understand that not reusing parsers has a memory impact. I also noticed PRs like #57938 and #59621 that might fix this thing.
This is an issue when using libraries like Datadog's dd-trace, which store tracing information in AsyncLocalStorage