-
Notifications
You must be signed in to change notification settings - Fork 419
Memory leak with Node.js 24 due to AsyncContextFrame + enterWith() #3870
Description
Memory leak with Node.js 24 due to AsyncContextFrame + enterWith()
Description
Node.js 24 changed the default AsyncLocalStorage implementation to use AsyncContextFrame (via nodejs/node#55552). Under this new default, the enterWith() method — which the New Relic agent relies on — causes memory leaks in long-running applications.
The New Relic agent calls enterWith() directly in its context manager:
| this._asyncLocalStorage.enterWith(newContext) |
setContext(newContext) {
this._asyncLocalStorage.enterWith(newContext)
}With the new AsyncContextFrame implementation, enterWith() behaves differently than it did under the previous AsyncLocalStorage implementation. Specifically, AsyncResource instances no longer reflect enterWith() mutations made after their construction — they snapshot the storage at construction time. This behavioral change, combined with the frequent enterWith() calls the agent makes on every instrumented function, leads to context objects being retained longer than expected and accumulating in memory.
There is an upstream Node.js issue tracking this behavioral change: nodejs/node#58204.
Additionally, enterWith() is marked as Stability: 1 - Experimental in the [Node.js v24 documentation](https://nodejs.org/docs/latest-v24.x/api/async_context.html#asynclocalstorageenterwithstore), which suggests it may not be the most reliable foundation for context propagation going forward.
Steps to reproduce
- Run any Node.js application instrumented with
newrelicon Node.js v24.0.0+ - Generate sustained traffic over time
- Observe heap memory growing continuously without being reclaimed by GC
The core issue can be demonstrated in isolation with this minimal script (from nodejs/node#58204):
const { AsyncLocalStorage, AsyncResource } = require('async_hooks')
const { strictEqual } = require('assert')
const storage = new AsyncLocalStorage()
storage.enterWith(1)
const ar = new AsyncResource('test')
ar.runInAsyncScope(() => {
storage.enterWith(2)
ar.runInAsyncScope(() => strictEqual(storage.getStore(), 2))
})
// Fails on Node 24: expected 2 but got 1Expected behavior
Memory usage should remain stable over time, with context objects being properly garbage-collected after their associated transactions complete.
Actual behavior
Heap memory grows continuously. Context objects set via enterWith() are retained because the AsyncContextFrame implementation snapshots storage at AsyncResource construction time rather than reflecting later enterWith() mutations.
Environment
- Node.js version: v24.0.0+
- New Relic agent version: tested with v12.x and 13.x
- OS: any
Temporary workaround
Starting the application with the --no-async-context-frame flag reverts to the previous AsyncLocalStorage implementation and avoids the leak:
node --no-async-context-frame app.js
Suggested fix
Consider replacing the enterWith() usage in async-local-context-manager.js with AsyncLocalStorage.run(), which is the stable API and works correctly under both the old and new AsyncContextFrame implementations. Alternatively, the Node.js docs suggest using tracingChannel as a modern replacement for patterns relying on enterWith() + AsyncResource.
Related issues
- nodejs/node#55552 — PR making
AsyncContextFramethe default - nodejs/node#58204 —
AsyncResourceno longer respectsenterWith()underAsyncContextFrame - Node.js v24 docs:
enterWith()stability — marked as Experimental
Metadata
Metadata
Assignees
Labels
Type
Projects
Status