Next.js 15 App Router Caching(Part 2): Data cache
Learn how to cache requests efficiently in Next.js 15 App Router using built-in tools like fetch cache and revalidation.
Next.js 15 App Router offers several layers of caching:
- Request Memoization
- Data Cache(current post)
- Full Route Cache
- Router Cache
- React Cache
Let's dive into the second: Data Cache
What is Data Cache?
In the Next.js 15 App Router, the data cache is a mechanism for caching fetch requests. Unlike request memoization, which stores results in RAM during a single request, the data cache stores results on disk in the .next/cache
folder and persists across many requests.
How it works?
Next.js extends the native fetch
API on the server. You can control caching by passing options to fetch
cache
next.revalidate
next.tags
Example of cached fetch
export const dynamic = 'force-dynamic'// Force page to be SSR for the example.
async function HomePage() {
const response = await fetch('https://kolarclub.com/api/posts', {
cache: 'force-cache', // Request will be cached
next: {
revalidate: 120, // Optional. Can be used for automatic revalidation. Given in seconds
tags: ['posts'], // Optional. Can be used to revalidate with revalidateTag('posts')
},
});
return null;
}
Let’s say we have two browsers (A and B), and the above SSR HomePage
:
- Browser A opens https://kolarclub.com/: the
fetch
runs and hits the API. - Browser B visits the page within 120 seconds:
fetch
returns from the cache — no API call. - After 120 seconds, the cache expires and the process starts again.
Where Data Cache works?
- Server Components
- Route Handlers
- Server Actions
How long is it cached?
Until it is revalidated. The cache can persist across:
- Multiple requests
- Multiple deployments
How can be revalidated?
Clearing of the data cache is called "revalidation". The revalidation depends on how it is configured
- Automatic revalidation
Use next: { revalidate: N }
— revalidates every N seconds.
- Manual revalidation
Use next: { revalidate: false }
and revalidate with revalidatePath
and revalidateTag
In both cases cache can be revalidated manually with revalidatePath
and revalidateTag
Lets explain cache, revalidate and tags
With this 3 options passed to fetch
you can configure caching of the request.
fetch('...', {
cache:'no-store' // 'force-cache' | 'no-store'
next:{
revalidate: undefined // false | number | undefined
tags: undefined // string[] | undefined
}
})
Option cache
'force-cache'
– cache the response'no-store'
– do not cache anything
Default in Next.js 15 is 'no-store'
, meaning fetches are not cached unless explicitly set.
Option revalidate
number
– cache response and revalidate after X secondsfalse
– do not revalidate automatically, only viarevalidateTag()
orrevalidatePath()
0
– do not cache at all (acts like'no-store'
)
Default is undefined
, which behaves like false
. Use false
explicitly to make this behavior clear.
Option tags
It is array of strings. Used to have fine-grained control over caching behaviour.
- if you call
revalidatePath
allfetch
requests on the page will be revalidated. - if you call
revalidateTag
you choose the exactfetch
requests
Examples
Here it is few examples that I wondered about.
SSR page with cached fetch (revalidates on demand)
export const dynamic = 'force-dynamic';
export default async function Page() {
const res = await fetch('https://example.com/api/items', {
cache: 'force-cache',
next: {
revalidate: false,
tags: ['items'],
},
});
return null;
}
Static page with cached fetch (can still revalidate manually)
export const dynamic = 'force-static';
export default async function Page() {
const res = await fetch('https://example.com/api/items', {
cache: 'force-cache',
next: {
revalidate: false,
tags: ['items'],
},
});
return null;
}
Both revalidatePath()
and revalidateTag()
will revalidate the page and fetch
because the page is generated at build time.
SSR page with no cache
export const dynamic = 'force-dynamic';
export default async function Page() {
const res = await fetch('https://example.com/api/items', {
cache: 'no-store',
next: {
revalidate: false, // This is useless
},
});
return null;
}
Where Is the cache stored?
- Locally: in
.next/cache
after runningnext build
- On Vercel: in the Edge Network for fast global access
How to test it locally?
In development mode (next dev
), caching behaves inconsistently.
You should run: next build && next start
For myself I made helper script: "bs": "next build && next start"
How I understood every aspect of Next.js 15 App Router?
I created empty project where I played and tested the caching behaviour. Here are my files
import RevalideteButton from './components/RevalidateButton';
export const dynamic = 'force-dynamic';
export default async function Page() {
const res = await fetch('http://localhost:3000/api', {
cache: 'force-cache',
next: {
revalidate: 200,
tags: ['test'],
},
});
const data = await res.json();
console.log('---Page: Response received!', data);
return (
<div className="flex flex-col max-w-md">
<div>{data}</div>
<RevalideteButton path="/dev" label="Revalidate Path" />
<RevalideteButton tag="test" label="Revalidate Tag" />
</div>
);
}
'use client';
import { revalidate } from '../actions/revalidate';
export default function RevalidateButton({ tag, path, label }) {
return (
<button
onClick={() => revalidate(tag, path)}
className="btn btn-ghost"
>
{label}
</button>
);
}
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function revalidate(tag?: string, path?: string) {
if (tag) revalidateTag(tag);
if (path) revalidatePath(path);
if (!tag && !path) throw new Error('Nothing to revalidate');
}
export async function GET() {
console.log('---Route: Request handled!')
return Response.json('Hello, world!', { status: 200 })
}
On first request you’ll see:
---Route: Request handled!
---Page: Response received!
On cached fetches (after first request), only the page log appears:
---Page: Response received!
One more thing
If you dont have access directly to the fetch
function, for example using some client like await cms.getPosts()
, you still can cache the request same way but using unstable_cache
from next. This will be explored more on the next posts here in the blog.
Final Tips
Understanding of data cache in Next.js App Router requires you to play with it so take the most of the examples and write them on your project!