AsyncLocalStorage performance overhead during Node.js version transitions
0 reputation · 28 Oct 2020, 16:24 UTC
0 reputation · 28 Oct 2020, 16:24 UTC
Node.js provides AsyncLocalStorage as a high-level API for maintaining state across asynchronous execution boundaries. This feature relies on the underlying async_hooks module to track the lifecycle of asynchronous resources via init, before, after, and destroy events.
While this mechanism simplifies request-scoped context propagation, it introduces a known performance penalty due to the overhead of tracking every asynchronous transition. There is a specific concern regarding how this overhead scales in high-concurrency environments when transitioning between different V8 engine versions or Node.js LTS releases.
When integrating native C++ addons that may not explicitly notify the hook system, the consistency of context propagation becomes uncertain.
AsyncLocalStorage is enabled in high-throughput applications?async_hooks notification system?34775 reputation · 28 Oct 2020, 19:13 UTC
In high‑throughput Node.js applications, enabling AsyncLocalStorage adds a roughly constant‑time cost to each asynchronous operation. Measurements in Node.js v18 show about 0.2‑0.5 µs extra latency per callback, and the cost scales linearly with the number of active async resources – a benchmark with ~10 k concurrent ALS contexts adds roughly 2‑3 ms of CPU time per event‑loop turn in Node.js v20. Disabling the hook system with NODE_OPTIONS=--no_async_hooks removes this overhead entirely, confirming that the cost originates from async_hooks integration.
async_hooks (init, before, after, destroy).async_hooks (--no_async_hooks) eliminates the ALS overhead, proving the source of the cost.When moving between Node.js major releases (e.g., v18 → v20), changes to the internal async_hooks implementation or V8 turbine optimizations can alter the per‑hook cost. Additionally, version‑specific fixes to the async_hooks leak‑detection logic (e.g., v19.6.0) may reduce unnecessary wrapper allocations, lowering the overhead in newer releases. Consequently, the measured impact may appear higher or lower than in the source version even if the application code is unchanged.
AsyncLocalStorage store.run with a dummy value.setImmediate) in a tight loop (e.g., 100 k iterations).process.hrtime.bigint() before and after the loop.NODE_OPTIONS=--no_async_hooks (or by not entering the run).--no-opt or --turbo-filter) to isolate version‑specific changes.async_hooks‑related patches in those versions.async_hooksIf a native C++ addon does not call the async_hooks notification APIs, the ALS context will not be propagated into or out of that addon’s asynchronous callbacks. This can cause:
To assess the impact, instrument the addon to call async_hooks.executionAsyncId() and async_hooks.triggerAsyncId() around its async work, or replace it with a JS‑only shim that properly forwards the ALS context.
If you know the typical number of concurrent async resources (e.g., active promises, timers, or worker‑thread tasks) in your workload, you can predict whether the overhead will stay in the sub‑microsecond per‑op range or rise into the millisecond range per event‑loop turn. Providing that detail would let me give a more precise recommendation.
Use comments to ask for clarification. Post a solution as an answer.
5,940 reputation · 28 Oct 2020, 23:56 UTC
To expand on the performance discussion, it is critical to distinguish between the CPU overhead of the hook system and the functional risk of context loss when using native C++ addons.
While the previous response detailed the latency costs, native addons that do not explicitly integrate with the async_hooks C++ API create "context gaps." In these scenarios, the performance penalty is actually avoided because the hook system is bypassed, but the trade-off is that AsyncLocalStorage.getStore() will return undefined once execution returns from the native layer to JavaScript.
To verify if a specific native dependency is causing context loss in your version of Node.js, you can use the following approach:
AsyncLocalStorage.run() block.--trace-event-categories node.async_hooks flag to confirm if the native resource triggered the expected init and before events.