Every time you wonder “how does Datadog know which database query belongs to which HTTP request?”, the answer is async_hooks. It’s one of the most powerful and least understood Node.js APIs. It tracks the causal chain of async operations — when async resource A creates async resource B, async_hooks knows. This makes automatic request tracing, correlation IDs, and context propagation possible without any changes to application code. This guide focuses on Node.js async hooks,
⚡ TL;DR:
async_hooks.createHook()fires callbacks when async resources are created, destroyed, or when execution enters/leaves them.AsyncLocalStorage(built on async_hooks) is the production-ready way to propagate request context across all async operations without passing it as a parameter.
The Problem: Context Gets Lost Across Async Boundaries
// Without async_hooks: context must be threaded through every call
app.get('/users', async (req, res) => {
const requestId = req.headers['x-request-id'];
const users = await getUsers(requestId); // Must pass requestId
const enriched = await enrich(users, requestId); // Must pass requestId
res.json(enriched);
});
async function getUsers(requestId) { // Must accept and thread requestId
logger.info('Fetching users', { requestId }); // Tedious
return db.query('SELECT * FROM users');
}
// With AsyncLocalStorage: context available anywhere, automatically
const store = new AsyncLocalStorage();
app.get('/users', async (req, res) => {
await store.run({ requestId: req.headers['x-request-id'] }, async () => {
const users = await getUsers(); // No context passing!
res.json(users);
});
});
async function getUsers() {
const { requestId } = store.getStore(); // Context available here
logger.info('Fetching users', { requestId }); // Zero plumbing
return db.query('SELECT * FROM users');
}AsyncLocalStorage: Production Request Tracing
const { AsyncLocalStorage } = require('async_hooks');
const { randomUUID } = require('crypto');
const requestContext = new AsyncLocalStorage();
// Middleware: attach context to every request
app.use((req, res, next) => {
const ctx = {
requestId: req.headers['x-request-id'] || randomUUID(),
userId: req.user?.id,
startTime: Date.now(),
spans: [],
};
res.setHeader('x-request-id', ctx.requestId);
requestContext.run(ctx, next); // Everything in this request gets ctx
});
// Logger that automatically includes request context
const logger = {
info: (msg, extra = {}) => {
const ctx = requestContext.getStore() || {};
console.log(JSON.stringify({
level: 'info', msg, requestId: ctx.requestId,
userId: ctx.userId, ...extra
}));
}
};
// Database wrapper that auto-traces queries
async function query(sql, params) {
const ctx = requestContext.getStore();
const start = Date.now();
try {
const result = await db.query(sql, params);
ctx?.spans.push({ type: 'db', sql, duration: Date.now() - start });
return result;
} catch (err) {
logger.info('Query failed', { sql, error: err.message });
throw err;
}
}
// Now every function in the request automatically logs with requestId
app.get('/users/:id', async (req, res) => {
logger.info('Fetching user'); // requestId auto-included
const user = await query( // span auto-recorded
'SELECT * FROM users WHERE id = $1', [req.params.id]
);
logger.info('User fetched', { userId: user.id }); // requestId auto-included
res.json(user);
});Performance Monitoring with async_hooks
const { createHook, executionAsyncId } = require('async_hooks');
// Track active async resources — spot resource leaks
const activeResources = new Map();
const hook = createHook({
init(asyncId, type, triggerAsyncId) {
activeResources.set(asyncId, { type, triggerAsyncId, created: Date.now() });
},
destroy(asyncId) {
activeResources.delete(asyncId);
},
promiseResolve(asyncId) {
activeResources.delete(asyncId);
}
});
hook.enable();
// Check for resource leaks periodically
setInterval(() => {
const now = Date.now();
const old = [...activeResources.entries()]
.filter(([, r]) => now - r.created > 10000) // Older than 10s
.map(([id, r]) => `${r.type}(${id})`);
if (old.length > 100) {
console.warn('Possible resource leak:', old.length, 'old resources');
}
}, 5000);Async Hooks Cheat Sheet
- ✅ Use
AsyncLocalStoragefor request context — it’s the stable, production-ready API - ✅
store.run(ctx, fn)to attach context to a scope - ✅
store.getStore()to retrieve context anywhere in the async chain - ✅ Use
createHookonly for low-level instrumentation (APM tools) - ✅ AsyncLocalStorage survives: await, setTimeout, setInterval, event emitters
- ❌ Don’t use async_hooks for hot paths — it adds ~10-20% overhead per async operation
- ❌ Don’t store large objects in AsyncLocalStorage — context is held for the lifetime of all child async ops
Async hooks are most powerful in the context of the Node.js event loop internals guide — understanding how async resources are scheduled makes async_hooks callbacks predictable. For the streaming use case, Node.js streams create async resources that async_hooks tracks, which is how APM tools measure stream processing time. External reference: Node.js AsyncLocalStorage documentation.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
You just read something most developers never learn. Get more secrets like this delivered every week — JavaScript internals, Python optimizations, AWS architectures, system design, and AI workflows.
Related: Node.js Security Best Practices: Protect Your API From the Most Common Attacks · Day 32: Bedrock Converse API Tool Choice in Production · Day 11: Tool Use as an API, Not a Prompt Trick
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
