Master Next.js request memoization with React cache and AsyncLocalStorage. Learn to stop redundant data fetching and optimize your Server Components today.
Last month, I spent three days debugging a dashboard that was hitting our internal API roughly 40 times on a single page load. The culprit wasn't bad logic; it was a deeply nested tree of Server Components, each independently requesting the same user profile and permissions data.
In a traditional SPA, you'd reach for TanStack Query or SWR. In the App Router, those tools don't quite fit the server-side lifecycle. If you're building production-grade Next.js applications, you need to master request-scoped memoization to keep your server-side performance predictable.
When you pass data down through props, you create a strict coupling between parent and child components. Sometimes, that's fine. But when your UI is highly modular, you often end up with leaf components that need access to global context—like the current session, active locale, or feature flags.
If you fetch this data inside every leaf component, you're triggering redundant network requests. Even with HTTP/2 multiplexing, you're wasting CPU cycles on the server, increasing latency, and hammering your database. We initially tried passing a "data object" as a context provider, but that forced us to fetch everything at the root, which hurt our time-to-first-byte (TTFB). We needed something that felt like global state but lived only for the duration of a single request.
The simplest way to handle this in Next.js is the cache function from the react package. It wraps a function and returns a version that memoizes the result based on the arguments passed to it.
JAVASCRIPTimport { cache } from CE9178">'react'; export const getUser = cache(async (id) => { const res = await db.user.findUnique({ where: { id } }); return res; });
When you call this inside a Server Component, React ensures that if getUser is invoked with the same id during the render pass, it returns the cached promise rather than firing a new request. It’s clean, it’s native, and it’s specifically designed for Next.js.
However, cache has a limitation: it only works if you share the exact same function reference across your component tree. If you're building a library or a complex data layer, you need a more robust way to handle context.
When cache isn't enough—for instance, when you need to store request-specific headers or transaction IDs that aren't arguments to a function—AsyncLocalStorage is your best friend. It allows you to track state across asynchronous calls within a single request execution context.
I use it to create a "Request Store." Here’s how I typically set it up:
JAVASCRIPTimport { AsyncLocalStorage } from CE9178">'node:async_hooks'; const requestStore = new AsyncLocalStorage(); export function getRequestData() { return requestStore.getStore(); } export function runWithRequest(data, callback) { return requestStore.run(data, callback); }
By wrapping your entry point—usually in a layout or a custom middleware-like pattern—you can inject a store that any component deep in the tree can access. This is particularly useful for Next.js apps that need to maintain state across complex data pipelines without prop-drilling or redundant fetches.
While these patterns are powerful, they aren't free.
AsyncLocalStorage store directly.In my current stack, I use cache for pure data fetching (like database queries) and AsyncLocalStorage for request-scoped metadata (like user roles or current tenant IDs). This split keeps the code readable while preventing the dreaded "waterfall" of requests that plagues many server-side architectures.
Next time, I'd probably look closer at whether I can push more of this into standard Next.js fetch caching, which handles deduplication automatically. However, for internal database calls or third-party SDKs that don't use fetch, the combination of cache and AsyncLocalStorage remains the gold standard for performance.
Does React cache persist across different user requests? No. It’s scoped to the request lifecycle. Once the server finishes rendering the page and sends the response, the cache is cleared.
Can I use AsyncLocalStorage in Client Components?
No. AsyncLocalStorage is a Node.js-only API. It will not work in the browser, and trying to import it in a Client Component will cause a build error.
Is React cache better than standard memoization? Yes, because it’s aware of the React render lifecycle. It handles the cache invalidation and promise resolution automatically, which is much safer than writing your own manual memoization wrappers.
Master Next.js traffic shadowing to safely deploy Canary releases. Learn how to use Edge Middleware to mirror requests for testing Server Components at scale.