logo

Global Async Contexts: Tracking User Sessions with Node.js's AsyncLocalStorage

Node.js

AsyncLocalStorage

Session Management

Request Tracking

User Sessions

|

5 January 2025

One of the painful parts of managing request-scope data, such as user sessions or request IDs, is the asynchronous nature of Node.js. It is pretty easy to lose track of the context with naked eyes when you have nested callbacks or multiple asynchronous tasks running. Thankfully, the AsyncLocalStorage API provides a way to handle these challenges.

In this blog post, we are going to explore how to use AsyncLocalStorage for tracking user sessions along with request IDs in Node.js applications efficiently.

What is AsyncLocalStorage?

AsyncLocalStorage is a part of the async_hooks module that appeared in Node.js version 13.10. It allows you to create storage to persist across asynchronous operations within the same context. Think of this as a way to maintain a sort of "global" state for individual requests without resorting to some cumbersome workarounds.

Key Benefits

  • Context Isolation: Each request gets its own isolated storage.
  • Thread Safety: Works seamlessly even under multi-threading.
  • Ease of Use: Tracks request-specific data.

Installing Necessary Packages

Ensure that you have Node.js 14 or higher to use AsyncLocalStorage.

If you are starting a new project:

pnpm init -y
pnpm add express

We will use Express.js as an example, but AsyncLocalStorage also supports any Node.js framework.

Basic Setup

Let's dive into implementation. First of all, import the AsyncLocalStorage class from the async_hooks module:

const { AsyncLocalStorage } = require("async_hooks");
var express = require("express");
const app = express();

const asyncLocalStorage = new AsyncLocalStorage();

Here, asyncLocalStorage will handle context for our requests.

Tracking Request IDs

In web development, an ID is commonly assigned to every request. It helps in debugging and logging.

Middleware Example

Create a middleware that assigns a unique ID to every incoming request:

const { v4: uuidv4 } = require("uuid");

app.use((req, res, next) => {
  asyncLocalStorage.run(new Map(), () => {
    const requestId = uuidv4();
    asyncLocalStorage.getStore().set("requestId", requestId);
    next();
  });
});

Accessing the Request ID

Later in the request lifecycle, you can pull the request ID out of storage:

app.get("/", (req, res) => {
  const store = await asyncLocalStorage.getStore();
  const requestId = store ? store.get('requestId') : 'Unknown';
  res.send(`Request ID: ${requestId}`);
});

Expected Output

When you hit the root endpoint, you'll get a unique request ID:

Request ID: a1b2c3d4-e5f6-7890

Managing User Sessions

You can use AsyncLocalStorage to track user sessions across asynchronous operations.

Middleware to Set User Data

Suppose you have an authentication layer that extracts user info:

app.use((req, res, next) => {
  const user = { id: 123, name: "John Doe" }; // Mock user data

  asyncLocalStorage.run(new Map(), () => {
    asyncLocalStorage.getStore().set("user", user);
    next();
  });
});

Retrieving User Data

Access user data from any handler or service:

app.get("/profile", (req, res) => {
  const store = await asyncLocalStorage.getStore();
  const user = store ? store.get("user") : null;

  if (user) {
    res.send(`Hello, ${user.name}!\n`);
  } else {
    res.status(401).send("Unauthorized");
  }
});

Caveats

  1. Nested Contexts: Be very careful when you nest calls with asyncLocalStorage.run, because it might replace an already established context.
  2. Performance Overhead: Although AsyncLocalStorage is quite efficient, excessive usage can bring in performance problems for high-load applications.
  3. Non-async Functions: It only works within async functions. The sync tasks will not hold the context.

Debugging Context Issues

If you encounter issues with lost context, ensure that:

  • All async operations inside a request are wrapped by asyncLocalStorage.run.
  • You’re not unintentionally clearing the store.

Use the code snippet below for debugging:

console.log(asyncLocalStorage.getStore());

Conclusion

AsyncLocalStorage simplifies and makes more robust the management of request-specific data in Node.js. It enables you to track request IDs and user sessions with ease, which makes your application architecture easier and debugging easier. Of course, it has its limitations, but it is an incredibly powerful tool for modern Node.js development.

© 2025 Pikazord. All rights reserved.