This article was originally published on the previous Kolarclub website on 30 April 2025. It was transferred here on 22 July 2026 and revised for clarity and technical accuracy.
Next.js 15 App Router has several caching layers:
- Request Memoization
- Data Cache
- Full Route Cache
- Router Cache
- React
cache
This part focuses on the Data Cache: the persistent server-side cache for data returned by fetch.
Data Cache vs. Request Memoization
Request memoization and the Data Cache can both reuse a fetch result, but their lifetimes are different.
| Mechanism | Scope | Typical lifetime |
|---|---|---|
| Request Memoization | One React server render | Until that render finishes |
| Data Cache | Shared server-side cache | Across incoming requests and, depending on the platform, deployments |
Request memoization happens automatically for matching GET and HEAD requests during rendering. The Data Cache is something you explicitly opt into in Next.js 15.
Opting a Request into the Data Cache
In Next.js 15, a server-side fetch without an explicit caching option behaves like cache: 'no-store'. Use force-cache when you want the response stored:
export default async function Page() {
const response = await fetch('https://example.com/api/posts', {
cache: 'force-cache',
})
const posts = await response.json()
return <PostList posts={posts} />
}
On a cache miss, Next.js requests the data and stores the response. On later requests, a matching fetch can read that response from the Data Cache.
The physical storage is an implementation and hosting detail. Treat the Data Cache as a Next.js abstraction instead of relying on a specific local directory or storage product.
Time-Based Revalidation
Use next.revalidate to allow a cached result to become stale after a number of seconds:
const response = await fetch('https://example.com/api/posts', {
next: { revalidate: 120 },
})
This means the response can be reused for 120 seconds. After that period, the next request may receive the stale value while Next.js refreshes it in the background. If the refresh succeeds, later requests receive the new value.
next.revalidate accepts:
- A positive number — cache and revalidate after that many seconds.
false— cache indefinitely until an on-demand invalidation or deployment behavior removes it.0— do not use the Data Cache for this request.
Do not combine contradictory options such as cache: 'no-store' and a positive revalidate value.
Tag-Based Revalidation
Tags let you invalidate a related group of cached requests:
const response = await fetch('https://example.com/api/posts', {
cache: 'force-cache',
next: {
tags: ['posts'],
},
})
When the content changes, a Server Action or Route Handler can invalidate the tag:
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshPosts() {
revalidateTag('posts')
}
Use a tag when several pages or components depend on the same logical data set.
Path-Based Revalidation
Use revalidatePath when the route is the natural unit you want to refresh:
'use server'
import { revalidatePath } from 'next/cache'
export async function refreshBlog() {
revalidatePath('/blog')
}
A simple rule of thumb:
- Use
revalidateTagfor shared data such asposts,products, oruser:123. - Use
revalidatePathwhen you specifically need to refresh one page or route subtree.
Cached Data on a Dynamically Rendered Route
The Data Cache and the Full Route Cache are independent. A route can render for every incoming request while still reusing selected data:
export const dynamic = 'force-dynamic'
export default async function Page() {
const response = await fetch('https://example.com/api/catalog', {
cache: 'force-cache',
next: { tags: ['catalog'] },
})
const catalog = await response.json()
return <Catalog items={catalog} />
}
The page is rendered on every request, but the catalog request can still come from the Data Cache.
Uncached Data
Use no-store for request-specific or frequently changing data:
const response = await fetch('https://example.com/api/current-status', {
cache: 'no-store',
})
The response is fetched again for each incoming render. Matching calls can still be memoized within that one render.
Testing Cache Behavior
Development mode adds conveniences for hot reloads and can reuse fetched data in ways that make cache testing confusing. For reliable testing, use a production build:
npm run build
npm run start
Add a log at the actual data source or inspect its request count. A log in the page component only proves that the page rendered; it does not prove whether fetch hit the Data Cache.
Practical Checklist
Before caching a request, answer four questions:
- Can different visitors safely share this response?
- How stale may the data become?
- Should an event invalidate it immediately?
- Is a tag or a route path the better invalidation boundary?
Those answers usually lead directly to no-store, force-cache, time-based revalidation, or on-demand revalidation.
For the precise version-specific behavior, see the Next.js 15 caching guide and the Next.js 15 fetch reference.