Managing Request Context in Node.js with async_hooks without Monkey Patching
This article explains how to propagate request‑level context in a Node.js HTTP service by building a call‑tree with async_hooks, avoiding monkey‑patching, handling lifecycle quirks, preventing memory leaks, and using reference‑counted cleanup to ensure reliable context queries.
Overview
The article continues a previous discussion on using async_hook to pass request context through business logic and explores a solution that does not rely on monkey‑patching. It demonstrates how to construct a call‑tree from async IDs, set and query context, and manage the lifecycle of async resources.
Call‑Stack Technique
By visualising the call‑stack as a dependency tree, each node represents a function call. Middleware placed at the beginning of the HTTP stack appears in the lower‑left part of the tree, executing before later calls.
Building the Tree
In the async_hook callback, a new node is created for every asyncId that triggers another asyncId , mirroring the diagram shown in the original article.
Setting Context
A dedicated middleware is inserted as the first element in the middleware chain. For each incoming request it:
Assigns the required context to the current asyncId .
Walks up the ancestor chain; if an ancestor lacks context, it copies the newly set context to that ancestor.
The algorithm runs in O(1) average time because the number of recursive look‑ups never exceeds the call depth.
Querying Context
To retrieve context, the algorithm starts from the current asyncId and climbs the parent chain until it finds a node with stored context, copying it to each visited node on the way back.
Advantages and Disadvantages
Advantages: no dependency on specific libraries, same computational complexity as monkey‑patching, and avoids invasive patches.
Disadvantages: more complex implementation, may fail if two independent requests share the same async resource, and requires careful reference management to prevent memory leaks.
Lifecycle Issues with async_hooks
The article points out two surprising behaviours observed in Node.js 10:
Some async resources emit before / after callbacks while others do not.
Ancestor destroy events can fire before descendant before / after events.
These quirks can leave the top of the call‑stack without associated context if the context is cleared on destroy . To avoid memory leaks, the article proposes a reference‑counted cleanup:
init
When a node is created, its context is marked as referenced and the parent’s reference count is incremented.
destroy
Remove the node’s own reference.
If no descendants reference the context, delete it and decrement the parent’s count.
Repeat the check up the tree.
Practical Problems Observed in Production
Long‑running keep‑alive HTTP connections prevented destroy from being called, causing a steady increase of unreclaimed async resources. The authors mitigated this with a generational garbage‑collection approach using two maps (old and new) and periodically swapping them every five minutes.
Conclusion
Using async_hook for context propagation simplifies tracing and statistics in micro‑service environments. The full implementation is available in the open‑source project envoy-node , and readers are encouraged to provide feedback.
Bitu Technology
Bitu Technology is the registered company of Tubi's China team. We are engineers passionate about leveraging advanced technology to improve lives, and we hope to use this channel to connect and advance together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.