Using AsyncLocalStorage in Next.js 🧰 ✚ ⚛️
Tags: nextjs, react, javascript, webdev
Published: 2023-06-18 08:40:06

## Introduction

In this article, we showcase how to introduce AsyncLocalStorage into your Next.js application!

> This post focuses on the new [Next.js App Router](https://nextjs.org/docs/app). If you are wanting to use the Pages Router (`pages/api` directory), you would need a slightly modified approach, which will be available in the linked repository at the end soon! The concept and general idea remains the same, only a slight difference in API in the API route handlers.
> 

## Why AsyncLocalStorage?

[AsyncLocalStorage](https://nodejs.org/api/async_context.html#asynchronous-context-tracking), introduced in Node.js (v13.10.0, v12.17.0), brings storage to asynchronous operations in JavaScript. It allows you to run a function or callback within a special environment with access to its own storage. This storage is accessible anywhere within the callback.

Why is this significant? Well, in multithreaded languages like Java or PHP, processes are able to create threads, each of which has its own thread local storage in memory. Each of these threads can execute the same logic and interact with memory without having to worry about overriding each other’s storage! This is perfect for use cases such as request tracing in a backend web server since each thread can create and save a request ID, that can be referenced and logged by any functions the thread executes. Looking at such logs, we can trace a request from beginning to end using such a trace ID.

In JavaScript, however, there is only a single thread. All memory is accessible and modifiable by callbacks and methods. In a web server, when a single thread receives a request, it fires an event that is handled asynchronously by callbacks. Callbacks do not have their own memory to store something such as a request ID. To achieve something such as a unique ID for each request, we would have to pass an ID down through all function calls, which is not always doable. Even where possible, we would be lugging around extra data in memory for each function call.

Instead, AsyncLocalStorage allows asynchronous callbacks to behave like threads - having their own local private store which can be accessed anywhere within the execution of the callback!

This opens the doors to several features such as request tracing and more. This [example from the Node.js](https://nodejs.org/api/async_context.html#class-asynclocalstorage) documentation drives it home! Further, if you have had a bit of time to play with Next.js server actions, you will come by the `headers()` and `cookies()`, helpers which give access to the current request’s headers and cookies. Under the hood these magic function calls are made possible via AsyncLocalStorage.

## Using AsyncLocalStorage in Next.js Routes

So, how can we introduce ALS to Next.js Route Handlers? We can do so in 3 quick steps with the `@nextwrappers/async-local-storage` library! In the next section, we will also discuss a full breakdown of how the library works to try on your own as well.

Here’s what to do:

1. Step 1: Install the `@nextwrappers/async-local-storage` package
    
    ```bash
    npm install @nextwrappers/async-local-storage
    ```
    
2. Import the package and call it with an initializer to set the data for the async local storage. The initializer provides access to the same request object supplied to the request.
    
    ```tsx
    import { asyncLocalStorageWrapper } from "@nextwrappers/async-local-storage"
    
    export const {
    	wrapper: asyncLocalStorageWrapped,
    	getStore
    } = asyncLocalStorageWrapper({
    	initialize: (req) => "12345" // Any data you want
    })
    
    ```
    
3. Wrap the route handler with the wrapper as such, and that’s it!
    
    ```tsx
    // app/api/.../route.ts
    import { asyncLocalStorageWrapped, getStore } from "lib/wrappers"
    
    export const GET = asyncLocalStorageWrapped((request: NextRequest) => {
    	const asyncLocalStorageValue = getStore()
    	
    	console.log(asyncLocalStorageValue) // => "12345"
    	
    	return (...)
    })
    
    ```
    

Now we have AsyncLocalStorage setup for requests made to our API route. 

When we call `getStore()` anywhere in the code that is executed by the route handler, we can access the store we initialized with the wrapper.

> Tip: A useful tip is using a non-primitive value in your store, such as an object or map, on which you can store multiple properties. The return type of `getStore` is strongly typed from the inferred return of `initialize`.
> 

## Breakdown (Implementation)

So how does this work? The implementation of `@nextwrappers/async-local-storage` is very straightforward. Let’s take a look at some of the helper functions that make this possible.

### 1. The AsyncLocalStorage runner: `AsyncLocalStorage.run`

This function takes the store or `data` to be “attached” to a callback, the `callback` function to run with the store, and finally, the arguments (`args`) to be passed to the `callback`. 

> What this does is simply run your callback or function on your behalf, but first “attach” the store to the callback so it can be accessed ANYWHERE within the callback: as deep as possible. If A calls B, then B calls C, C will still have access to the store.
> 

I have defined a small type-safe wrapper function for this, though this is not absolutely necessary!

```tsx
/**
 * Runs a callback within the async local storage context
 * @param storage The async local storage instance
 * @param store The data to store
 * @param callback The callback to run within the async local storage context
 * @param args The arguments to pass to the callback
 * @returns The return value of the callback
 */
function runWithAsyncLocalStorage<
  Store = unknown,
  ReturnType = unknown,
  Args extends any[] = any[]
>(
  storage: AsyncLocalStorage<Store>,
  store: Store,
  callback: (...args: Args) => ReturnType,
  args: Args
) {
  return storage.run(store, callback, ...args);
}
```

Now that we have this piece, the next piece is to create the actual wrapper around the route handler.

This wrapper will handle initializing the async local store, before executing the route handler itself. 

For creating this wrapper, we use a helper from `@nextwrappers/core` to make defining the wrapper simple. For a breakdown of how that works, you can see the source code or check out this article.

### 2. The wrapper: `asyncLocalStorageWrapper`

This takes in some options such as `initialize` to initialize the store for the AsyncLocalStorage, and an optional `storage` parameter that allows providing your own `AsyncLocalStorage` instance.

It returns an object with three things:

1.  the AsyncLocalStorage `storage` instance,
2. a convenience `getStore` function which we can use to get the store and,
3. the `wrapper` we can use in our route handlers as we did already in the quick example above.

```tsx
/**
 * Creates an async local storage wrapper for a route handler
 * @param options The options include an optional async local `storage`
 * instance and an `initialize` function which receives the request
 * and returns the `store`
 * @returns
 */
export function asyncLocalStorage<Store>(
  options: AsyncLocalStorageWrapperOptions<Store>
) {
  const { initialize, storage = new AsyncLocalStorage<Store>() } = options;
  return {
    storage,
    getStore: () => storage.getStore(),
    wrapper: wrapper((next, req, ext) => {
      const store = initialize?.(req, ext);
      return runWithAsyncLocalStorage(storage, store, next, [req, ext]);
    })
  };
}
```

> In the above the `wrapper` helper from `@nextwrappers/core` gives us a middleware-like `next()` function which is really the route handler itself. Calling `next` essentially continues the request. Another nice thing about it is that when the wrapper is called, it returns back a plain route handler! This means you can wrap a route handler with multiple wrappers. See more in its documentation.
> 

## Performance Discussion

AsyncLocalStorage is not without its performance hits. It has improved significantly with newer versions of Node.js, but there is still a minor performance hit. 

Following this [issue](https://github.com/nodejs/node/issues/34493#issuecomment-845094849) on GitHub, I copied and adapted the benchmarking script to make fetch requests to a locally running Next.js application in production mode. The application has two GET routes, one running with AsyncLocalStorage, and the other without.

Here are the results from separate runs. The test comprises of firing as many requests as possible over a 10-second period first to a route with ALS and then to a route without ALS.

| ALS | No ALS | ALS Penalty (%) |
| --- | --- | --- |
| 112 | 113 | 0.88 |
| 117 | 114 | -2.63 |
| 111 | 114 | 2.63 |
| 113 | 114 | 0.88 |
| 113 | 114 | 0.88 |

In general, the no-ALS route consistently performs slightly better than the ALS route as expected. The penalty as a percentage however is very little, and I wouldn’t sweat it. This is just a basic benchmark and may not be reflective of real-world cases.

The specific version of the script used to perform the tests above can be found [here](https://github.com/rexfordessilfie/nextwrappers/blob/fcd75fb48e4a0e2778cbcd5f8e883cb0a2d180c2/examples/als/benchmark.js#L1).

## Conclusion

This concludes the discussion of setting up request tracing with AsyncLocalStorage in Next.js! Hope you found it useful! If you did, kindly leave a star on GitHub!

{% github https://github.com/rexfordessilfie/nextwrappers %}