DEV Community

Adarsh Hasnah
Adarsh Hasnah

Posted on

A Practical Introduction to AsyncLocalStorage in Node.js (With Real Use Cases)

AsyncLocalStorage (ALS) is a powerful but often misunderstood feature in Node.js. At its core, ALS provides a reliable way to maintain execution context across asynchronous operations—a challenge every Node.js backend eventually faces.

If you’ve ever needed to keep track of metadata such as request IDs, tenant identifiers, locales, or feature flags across nested async/await calls, ALS is the tool designed for that job.

This article explains what AsyncLocalStorage is, how it works, and how to apply it effectively using practical examples.


What Is AsyncLocalStorage in Node.js?

AsyncLocalStorage is part of the async_hooks module and functions similarly to thread-local storage in multi-threaded languages—except adapted for Node’s event-loop and asynchronous model.

It creates a context store that persists across all asynchronous boundaries triggered within a specific scope.

This enables predictable context propagation through:

  • Promises
  • Timers (setTimeout, setInterval)
  • Database clients
  • External API calls
  • Streams
  • Message queue handlers
  • Any nested async function

Core benefit: You no longer need to pass metadata manually through every function call.


Why AsyncLocalStorage Matters for Modern Node.js Applications

Maintaining context is important for:

  • Request correlation and structured logging
  • Distributed tracing
  • Multi-tenant routing
  • User or locale propagation
  • Feature flag consistency
  • Tracking background job executions

ALS is fundamentally about context propagation, not just logging.


Architecture: How ALS Propagates Context

Incoming Request / Task
        │
        ▼
┌────────────────────────┐
│ Initialize ALS Context │  ← store.run(...)
└───────────┬────────────┘
            │
            ▼
Handlers → Services → Async Calls → DB/API → Response
            │
            ▼
   The ALS context remains accessible everywhere
Enter fullscreen mode Exit fullscreen mode

ALS creates a scope once, and all subsequent async operations continue to use the same context.


Minimal AsyncLocalStorage Example

import { AsyncLocalStorage } from 'node:async_hooks';

const store = new AsyncLocalStorage();

store.run({ value: 123 }, () => {
  setTimeout(() => {
    console.log('Context:', store.getStore());
  }, 50);
});
Enter fullscreen mode Exit fullscreen mode

Output:

Context: { value: 123 }
Enter fullscreen mode Exit fullscreen mode

Even across timeout boundaries, the context is preserved.


Using AsyncLocalStorage in an Express Application

Initialize Context per Request

import { AsyncLocalStorage } from 'node:async_hooks';
import express from 'express';
import { randomUUID } from 'crypto';

const asyncStore = new AsyncLocalStorage();
const app = express();

app.use((req, res, next) => {
  const context = {
    requestId: req.headers['x-request-id'] || randomUUID()
  };

  asyncStore.run(context, () => next());
});
Enter fullscreen mode Exit fullscreen mode

Access the Context Later in the Flow

function getContext() {
  return asyncStore.getStore();
}

app.get('/users', async (req, res) => {
  await new Promise(resolve => setTimeout(resolve, 100));

  const ctx = getContext();

  console.log('Request ID:', ctx.requestId);

  res.json({ requestId: ctx.requestId });
});
Enter fullscreen mode Exit fullscreen mode

This is the classic request ID correlation pattern and is widely used for production observability.


Other Practical AsyncLocalStorage Use Cases

Beyond correlation IDs, ALS supports several operational patterns:

1. Multitenancy Context

asyncStore.run({ tenantId: 'acme' }, () => handleRequest());
Enter fullscreen mode Exit fullscreen mode

2. Locale & Internationalization

{ locale: req.headers['accept-language'] }
Enter fullscreen mode Exit fullscreen mode

3. Feature Flag Snapshot

{ featureFlags: evaluateFlags(user) }
Enter fullscreen mode Exit fullscreen mode

4. Background Job Tracking

worker.on('job', job => {
  asyncStore.run({ jobId: job.id }, () => processJob(job));
});
Enter fullscreen mode Exit fullscreen mode

These examples rely on the same mechanism: execution-scoped context.


When Should You Use AsyncLocalStorage?

Use Case ALS Recommended?
Request/Execution scoped metadata ✔ Yes
Request ID correlation ✔ Yes
Distributed tracing ✔ Yes
Multi-tenant context ✔ Yes
Passing large mutable objects ✘ No
Global config ✘ No

ALS should store lightweight metadata, not application state.


FAQ: Common Questions About AsyncLocalStorage

What is AsyncLocalStorage used for in Node.js?

It maintains execution-scoped context across async operations, enabling request correlation, multitenancy, tracing, and more.

Is AsyncLocalStorage only for request IDs?

No. Request IDs are just one example. ALS can hold any metadata relevant to the async execution scope.

Does ALS work with async/await?

Yes. ALS propagates through promises, timers, callbacks, and most async libraries.

Can ALS be used outside Express?

Yes. It works in Fastify, NestJS, message queues, CRON jobs, and any async workflow.

Is there a performance cost?

A small one, but acceptable for most workloads. ALS is used by major frameworks and tracing tools.


Summary

AsyncLocalStorage is a foundational tool for building reliable, observable Node.js applications. It provides a clean and efficient way to maintain execution context without manually passing metadata through every function. While request ID correlation is the most common pattern, ALS supports many other practical use cases such as multitenancy, localization, feature flags, and background job correlation.

If you're building production-grade Node.js services, understanding ALS will significantly improve your architecture, debugging capabilities, and observability.

Top comments (0)