Skip to main content
node24async-local-storageexpressobservability

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

Usama Amjid
6 min read
4035 words
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 reqId everywhere. 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)

bash
npm install pino

2) Create a context helper: context.js

javascript
// 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

javascript
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

javascript
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:

javascript
// 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

  1. Keep store small. Store only small, serializable values (ids, flags). Big objects increase memory pressure.
  2. Guard against undefined. Always use getStore() defensively (getStore() || {}) in shared helpers.
  3. Test edge cases. Include tests with timers, streaming responses, event emitters, and promise chains to ensure context survives.
  4. Avoid global singletons that hold store values. Don’t let long-lived objects capture store references.
  5. Monitor memory/GC. When first enabling ALS in a busy app, watch memory, GC, and CPU to spot regressions.
  6. 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.

Written by

Usama Amjid

Backend Engineer & Node.js Architect

Get in Touch