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
- Nested Contexts: Be very careful when you nest calls with
asyncLocalStorage.run
, because it might replace an already established context. - Performance Overhead: Although AsyncLocalStorage is quite efficient, excessive usage can bring in performance problems for high-load applications.
- 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.