Next.js AsyncLocalStorage: Type-Safe Request Context Injection
Next.js AsyncLocalStorage enables global request context in Server Components. Learn how to implement type-safe dependency injection for auth and traceability.

Last month, I spent about three days refactoring a messy data-fetching layer in a large Next.js application. We were passing userId and traceId through four layers of nested Server Components, eventually hitting a database utility that didn't even need the other props. It was a classic case of prop drilling, and it was brittle.
If you’ve been working with Server components vs client components: A practical guide, you know that Server Components don't have access to React Context. When you need to provide global request-scoped data—like an authenticated user object or a distributed tracing ID—without explicit prop passing, AsyncLocalStorage is your best friend.
Why AsyncLocalStorage for Request Context?
Node.js provides AsyncLocalStorage to store data across asynchronous calls. Since each Next.js request runs in its own execution context, we can store request-specific information at the entry point and retrieve it anywhere down the call stack.
The primary benefit is decoupling. Your lower-level data services shouldn't care about the HTTP request or the middleware headers. They just want the userId.
The Implementation Pattern
First, we define a store module. I prefer a dedicated request-context.ts file to keep the logic isolated.
TYPESCRIPTimport { AsyncLocalStorage } from CE9178">'node:async_hooks'; export interface RequestStore { userId: string; traceId: string; } export const requestContext = new AsyncLocalStorage<RequestStore>();
Next, we need a way to initialize this. In Next.js, the best place to seed this data is usually a custom middleware or a layout component that wraps your providers. However, because we are in the Node.js runtime, we can hook into the request lifecycle.
Achieving Type Safety
To ensure we don't accidentally access the store outside of a request, we wrap the getter. This is where TypeScript Branded Types: Enforcing Domain Integrity at Compile-Time can help, but for a simple store, a strict getter function is usually sufficient.
TYPESCRIPTexport function getRequestContext(): RequestStore { const store = requestContext.getStore(); if (!store) { throw new Error(CE9178">'RequestContext accessed outside of request scope'); } return store; }
The Trade-off: Runtime vs. Compile-time
We first tried to use a global singleton pattern, but it failed immediately during concurrent requests. AsyncLocalStorage solves the concurrency issue by binding the store to the async execution flow.
However, there’s a catch. You must ensure that your code calling getRequestContext is only executed during the request-response cycle. If you try to trigger this inside a static generation step (like generateStaticParams), you'll hit that Error we defined above. It’s an intentional trade-off; it forces you to acknowledge that certain data is only available at runtime.
Integrating with Server Actions
When you combine this with Next.js Server Actions: Implementing Type-Safe Mutations and Middleware, you get a very clean DX. You can have your server action call a service, which in turn calls getRequestContext() to fetch the userId, effectively "injecting" the dependency without the action signature becoming bloated.
How to use it in production
- Initialize in a Middleware or Root Layout: Wrap your application code with the
runmethod. - Access anywhere: Use
getRequestContext()in your deep service layers. - Type Safety: Ensure your
RequestStoreinterface is strictly defined.
This pattern makes testing significantly easier. You can mock the AsyncLocalStorage store in your unit tests without needing to spin up a full HTTP mock server for every single service-layer test.
What I'm still debating
I’m still not 100% sold on using this for everything. If the data is truly global and changes frequently, this is perfect. But if the data is only used by two components, keep it simple and pass it as a prop. Don't over-engineer your architecture just because you can.
I’ve also noticed that if you are using edge runtimes, AsyncLocalStorage behaves differently depending on the specific environment configuration. Always test your deployment pipeline to ensure the context propagation remains intact during streaming responses. It’s a powerful tool, but like any global state, it requires discipline to keep the codebase maintainable.