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
newrelic on 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 1
Expected 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
Memory leak with Node.js 24 due to AsyncContextFrame +
enterWith()Description
Node.js 24 changed the default
AsyncLocalStorageimplementation to useAsyncContextFrame(via nodejs/node#55552). Under this new default, theenterWith()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:node-newrelic/lib/context-manager/async-local-context-manager.js
Line 43 in 83adcc4
With the new
AsyncContextFrameimplementation,enterWith()behaves differently than it did under the previousAsyncLocalStorageimplementation. Specifically,AsyncResourceinstances no longer reflectenterWith()mutations made after their construction — they snapshot the storage at construction time. This behavioral change, combined with the frequententerWith()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
newrelicon Node.js v24.0.0+The core issue can be demonstrated in isolation with this minimal script (from nodejs/node#58204):
Expected 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 theAsyncContextFrameimplementation snapshots storage atAsyncResourceconstruction time rather than reflecting laterenterWith()mutations.Environment
Temporary workaround
Starting the application with the
--no-async-context-frameflag reverts to the previousAsyncLocalStorageimplementation and avoids the leak:Suggested fix
Consider replacing the
enterWith()usage inasync-local-context-manager.jswithAsyncLocalStorage.run(), which is the stable API and works correctly under both the old and newAsyncContextFrameimplementations. Alternatively, the Node.js docs suggest usingtracingChannelas a modern replacement for patterns relying onenterWith()+AsyncResource.Related issues
AsyncContextFramethe defaultAsyncResourceno longer respectsenterWith()underAsyncContextFrameenterWith()stability — marked as Experimental