We've covered full route cache, data cache and revalidation. Next, we look into router cache and request memoization.
Router cache
Router cache is client side cache, it lives in the browser's memory and consists of rsc payload. The goal of router cache is instantaneous navigation between pages. It does that by caching already visited pages and by prefetching pages that we might visit (the <Link /> component) and saving those in cache.
Each route is split into route segments: layouts (layout.tsx), loading states (loading.tsx) and pages (page.tsx). It's these route segments that actually get cached in the router cache. But only partly:
- Layouts are cached and reused on navigation (partial rendering).
- Loading states are cached and reused on navigation for instant navigation.
- Pages are not cached by default, but are reused during browser backward and forward navigation. [...]
I don't really know how a page can be reused but not cached.
Duration
I can't improve upon what the Next docs say about this, so I'm just going to copy it:
The cache is stored in the browser's temporary memory. Two factors determine how long the router cache lasts:
- Session: The cache persists across navigation. However, it's cleared on page refresh.
- Automatic Invalidation Period: The cache of layouts and loading states is automatically invalidated after a specific time. The duration depends on how the resource was prefetched, and if the resource was statically generated:
- Default Prefetching (prefetch={null} or unspecified): not cached for dynamic pages, 5 minutes for static pages.
- Full Prefetching (prefetch={true} or router.prefetch): 5 minutes for both static & dynamic pages.
While a page refresh will clear all cached segments, the automatic invalidation period only affects the individual segment from the time it was prefetched.
Note: this comes from Next 15 docs, more on this is a bit.
Invalidation
Again, from the docs:
There are two ways you can invalidate the Router Cache:
- In a Server Action: - Revalidating data on-demand by path with (revalidatePath) or by cache tag with (revalidateTag) - Using cookies.set or cookies.delete invalidates the Router Cache to prevent routes that use cookies from becoming stale (e.g. authentication).
- Calling router.refresh will invalidate the Router Cache and make a new request to the server for the current route.
Note: in tests in the previous chapters, I had issues with router cache persisting after revalidation from a server action. So don't take this for granted.
Some closing notes
You may be thinking I took it a bit easy in this chapter by just copy-pasting. True, it was easier but there are some reasons why I did it.
-
Nextexplained it well. There was nothing for me to clarify. - It's hard to write tests for this so I couldn't really offer any added value.
But the main reason is something else. When I started writing this series I was carefully studying the docs. But, when I started to write this chapter, I suddenly couldn't find the docs on router cache anymore.
What happened? Next updated the docs on caching (v16.2.1 at 25/03/2026). Everything we covered in the previous 9 chapters is now considered "Caching and Revalidation (previous model)". The new model is "cache components".
On top of that, Next renamed router cache to client cache (we approve) and also changed how it works. The parts from the docs I cited above are from Next 15.
Lastly, I'm not sure there is that much to say about router cache. I just works in the background. We will come back to client cache later because at this point I don't know nothing about cache components yet.
Request memoization
It's very common to need data from a particular fetch in multiple components within a single route. Request memoization removes the need to pass props or use context by caching the result of the fetch and reusing it during the render pass.
/page.tsx
Component1
Fetch 1
Fetch 2
Component2
Fetch 1
Component3
Fetch 2
In the above example, only two fetches will be made, fetch 1 and fetch 2. When Next hits the same fetch again, it will reuse the memoized result.
How does this differ from data cache then?
- When
Nextencounters a fetch, it will first checkrequest memoization. If there is a match it will return the match and not check thedata cache. If there is no match in the memo cache, thenNextwill check the data cache and put this match in the memo cache. -
Request memoizationhas a lifespan of the request cycle. So, a request is made, fetches are memoized and reused if needed, the route renders and therequest memoizationcache is purged.
So, the big point of memoization is that you can be 100% confident in making the same fetch over and over again in a singe route. It will not affect performance or cause higher load on some server endpoint.
Some notes
- Memoization is a server feature. It only works in server components.
- Memoization only applies to the GET method in fetch requests.
- Once a request lifecycle has ended,
memoizationis removed. That's weredata cachewill takes over. It persists between routes. - Memoization doesn't work in route handlers (route.ts), only in the
Reactcomponent tree.
Finally, memoization happens for fetches that have the same url and options. Maybe we should write a little test for this?
Little test
Note: the examples are available on github.
We're going to create an api endpoint and then call it multiple times inside a dynamic page. Why do we need an endpoint? So I can log when it gets called. Why dynamic? In a static route I would need an external server (you can't run and build at the same time) and that would complicate this example. Here is the route handler:
// app\memoization\api\route.ts
const user = {
id: 1,
name: 'Peter',
};
export const dynamic = 'force-dynamic';
export async function GET() {
console.log('Memoization/api called');
return Response.json(user);
}
A getUser function that calls this endpoint. Note, we pass fetch options as param:
// app\memoization\getUser.ts
type UserT = {
id: number;
name: string;
};
export async function getUser(fetchOptions: RequestInit) {
console.count('Running getUser');
try {
const res = await fetch(
'http://localhost:3000/memoization/api',
fetchOptions,
);
const user: UserT = await res.json();
return user;
} catch (error) {
console.error(error);
return null;
}
}
A <User /> component that calls getUser and renders name:
// components\memoization\User.tsx
import { getUser } from '@/app/memoization/getUser';
export default async function User({
fetchOptions,
}: {
fetchOptions: RequestInit;
}) {
const user = await getUser(fetchOptions);
if (!user) return <div>No user found</div>;
return <div>{user.name}</div>;
}
And finally a route:
// app\memoization\page.tsx
import User from '@/components/memoization/User';
export const dynamic = 'force-dynamic';
export default async function Page() {
return (
<>
<User fetchOptions={{}} />
<User fetchOptions={{}} />
<User fetchOptions={{}} />
<User fetchOptions={{}} />
<User fetchOptions={{ cache: 'no-store' }} />
<User fetchOptions={{ cache: 'no-store' }} />
<User fetchOptions={{ cache: 'no-store' }} />
<User fetchOptions={{ cache: 'no-store' }} />
<User fetchOptions={{ cache: 'force-cache' }} />
<User fetchOptions={{ cache: 'force-cache' }} />
<User fetchOptions={{ cache: 'force-cache' }} />
<User fetchOptions={{ cache: 'force-cache' }} />
<User fetchOptions={{ next: { revalidate: 1000 } }} />
<User fetchOptions={{ next: { revalidate: 1200 } }} />
<User fetchOptions={{ next: { revalidate: 1400 } }} />
<User fetchOptions={{ next: { revalidate: 1600 } }} />
<User fetchOptions={{ next: { tags: ['user-1'] } }} />
<User fetchOptions={{ next: { tags: ['user-1'] } }} />
<User fetchOptions={{ next: { tags: ['user-2'] } }} />
<User fetchOptions={{ next: { tags: ['user-2'] } }} />
</>
);
}
We log from getUser and from the route handler. Take a guess, what happens? Remember, request memoization is automatic for fetches that have the same url and options.
For me, the result was a bit surprising:
Running getUser: 1
...
Running getUser: 20
Memoization/api called
Even though the getUser function was invoked 20 times, the underlying API was only hit once. There is an interesting quirk here: React's memoization usually requires the URL and all options to be identical. Even though we varied the cache and next.revalidate options in our test, React (or Next.js's patched version of it) deduplicated them anyway. Great for performance!
Bonus points: Here's a little question: how many of these fetches ended up in data cache?
Again, a bit surprising, we have two entries in cache:
// .next\cache\fetch-cache\hash
{"kind":"FETCH","data":{"headers":{"connection":"keep-alive","content-type":"application/json","date":"Mon, 30 Mar 2026 14:10:04 GMT","keep-alive":"timeout=5","transfer-encoding":"chunked","vary":"rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch"},"body":"eyJpZCI6MSwibmFtZSI6IlBldGVyIn0=","status":200,"url":"http://localhost:3000/memoization/api"},"revalidate":31536000,"tags":[]}
// .next\cache\fetch-cache\hash
{"kind":"FETCH","data":{"headers":{"connection":"keep-alive","content-type":"application/json","date":"Mon, 30 Mar 2026 14:10:04 GMT","keep-alive":"timeout=5","transfer-encoding":"chunked","vary":"rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch"},"body":"eyJpZCI6MSwibmFtZSI6IlBldGVyIn0=","status":200,"url":"http://localhost:3000/memoization/api"},"revalidate":1000,"tags":[]}
One with revalidate 1000, the other 31536000 (default = 1 year). Can we explain this?
- The first 8 fetches have implicit or explicit no cache policies, so they don't get cached in
data cache. - The first fetch with force-cache should then be the one above with 1 year revalidate. The next 3 with force-cache will be memoized.
- This:
<User fetchOptions={{ next: { revalidate: 1000 } }} />is obviously the second one we found in cache. But the next 3 with values 1200, 1400 and 1600 are absent. Not sure about this. - The last four with tags user-1 and user-2 are absent. Maybe they hit memo cache, matched and did not proceed to check
data cache? I'm not sure.
It gets tricky but in the end it doesn't really matter. Some final pieces.
Cache and unstable_cache
React automatically performs request memoization for the fetch API. If you aren't using fetch, you can manually implement request memoization by wrapping a function with cache:
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id });
return item;
});
Also, note that you're not limited to DB or endpoint queries. It's also possible to cache heavy computational functions. But, beware here, cache is for request memoization, not for data cache.
To manually add items to data cache, Next provides the unstable_cache function. However, Next warns that this API was replaced with use cache which we will cover later.
Closing
This mostly concludes caching in Next - the old model. Before we start with the new model - use cache - I have some loose ends to tie and some other things to investigate. We'll do that first and then move on to the new model.
If you want to support my writing, you can donate with paypal.
Top comments (0)