AsyncLocalStorage in Node.js 24 — What it is, why it matters, and how to use it in production

What is AsyncLocalStorage?
AsyncLocalStorage (ALS) is a small built-in tool in Node.js that gives you a tiny, per-request “box” that follows an asynchronous flow. Think of it as a pocket of data that travels with your request across async/await, promises, callbacks, and many other async boundaries — so you don’t need to pass the same context (like a request id, user id, or tracing info) through every function manually. This is similar to thread-local storage in other languages, but designed for Node’s asynchronous model.
Starting with Node.js 24, ALS’s internal implementation changed: it now uses AsyncContextFrame by default. That switch makes context tracking faster and more reliable and avoids some older pitfalls tied to the lower-level async_hooks machinery.
Why it matters — benefits
- •Cleaner code: No need to pass
reqIdeverywhere. Handlers and helpers can read the current context from ALS. - •Better logging & tracing: Attach a request id automatically to logs and traces without changing function signatures.
- •More reliable async context: Node 24’s internal improvements make ALS less likely to lose context across complex async operations.
- •Security & stability: Node 24 removes some unsafe internal patterns and includes patches fixing earlier crash/DoS bugs related to async context. Upgrading can avoid certain edge-case server crashes.
Downsides & gotchas
- •Not a data store: Don’t stash large objects or big DB connections in ALS — it’s for small, request-scoped metadata.
- •Memory leaks if misused: Long-lived timers or global references that keep store values alive can hold memory unexpectedly.
- •Not a cross-process solution: ALS works per Node process — it doesn’t share context across separate processes, containers, or machines.
- •Library compatibility: Some older libraries relied on the old internals; test third-party middleware when upgrading Node.
Practical, production-ready guide — use with Express
Below is a straightforward pattern you'll want in a production Express app: assign a request id, run the ALS store for each request, and access that id from deep inside your stack (logger, DB helpers, etc.). This pattern is robust with async/await, promises, and common async flows.
1) Install a lightweight logger (example uses pino but any logger works)
npm install pino2) Create a context helper: context.js
// context.js
import { AsyncLocalStorage } from 'node:async_context'; // Node 24+ recommended module
// (It's also available via node:async_hooks for compatibility)
export const als = new AsyncLocalStorage();
export function getContext() {
return als.getStore() || {}; // always return an object (avoid undefined)
}
export function setInContext(key, value) {
const store = als.getStore();
if (store) store[key] = value;
}
3) Express middleware to create request scope: middleware/requestContext.js
import { v4 as uuidv4 } from 'uuid';
import { als } from '../context.js';
export function requestContextMiddleware(req, res, next) {
const reqId = req.headers['x-request-id'] || uuidv4();
const store = { reqId, startTime: Date.now() };
// Run the request inside the ALS scope. All downstream code can read store.
als.run(store, () => next());
}
4) Logger that reads from context: logger.js
import pino from 'pino';
import { getContext } from './context.js';
const baseLogger = pino({ level: process.env.LOG_LEVEL || 'info' });
export function log() {
const ctx = getContext();
// create a child logger with request id — inexpensive
return baseLogger.child({ reqId: ctx.reqId || 'no-req-id' });
}
5) Use it in routes and deeper helpers:
// server.js (snippet)
import express from 'express';
import { requestContextMiddleware } from './middleware/requestContext.js';
import { log } from './logger.js';
const app = express();
app.use(requestContextMiddleware);
app.get('/user/:id', async (req, res) => {
const logger = log();
logger.info('Handling GET /user');
// call some async service that doesn't need reqId passed explicitly
const user = await findUser(req.params.id);
logger.info({ userId: req.params.id }, 'Fetched user');
res.json(user);
});
// deeper helper
async function findUser(id) {
const logger = log(); // still has reqId available
logger.debug('Finding user in DB', { id });
// ... db calls ...
}
This pattern keeps your route handlers slim and gives any helper function access to the same request-scoped metadata without changing their signatures.
Production tips & best practices
- Keep store small. Store only small, serializable values (ids, flags). Big objects increase memory pressure.
- Guard against undefined. Always use
getStore()defensively (getStore() || {}) in shared helpers. - Test edge cases. Include tests with timers, streaming responses, event emitters, and promise chains to ensure context survives.
- Avoid global singletons that hold store values. Don’t let long-lived objects capture store references.
- Monitor memory/GC. When first enabling ALS in a busy app, watch memory, GC, and CPU to spot regressions.
- Upgrade safely. When moving to Node 24, run package tests: most libraries work, but some low-level hooks changed.
When to use ALS — quick decision guide
- •Use it for: request ids, tracing ids, per-request locale, short-lived flags, error correlation, contextual logging.
- •Don’t use it for: session storage, caching, large per-request state, or communication between processes.
Short summary / TL;DR
Node.js 24 brings a more efficient and more robust AsyncLocalStorage implementation (AsyncContextFrame by default), making request-scoped async context easier and safer to use. For Express apps, ALS lets you attach request ids and tracing metadata cleanly and reliably — just remember to keep the stored data small, test async edge cases, and monitor resource use after upgrading.
