You deployed a Next.js App Router app, called revalidatePath() after a mutation, and the page still shows stale data. Or you disabled caching on a fetch call and the page is somehow still cached. Or you migrated from Next.js 14 to 15 and everything that was working broke silently.
All of these happen because the App Router has four separate caching layers, and they each have their own invalidation story. Developers hit these bugs constantly because the mental model of "there is one cache" is wrong.
Here's what actually exists.
The four caching layers in Next.js App Router
Next.js App Router does not have a single cache. It has four distinct layers, each running in a different context with different lifetimes and different invalidation APIs. (Next.js caching docs)
| Layer | Where it runs | Lifetime | Invalidated by |
|---|---|---|---|
| Request Memoization | Server, per request | Request lifetime | Automatic |
| Data Cache | Server, persistent | Until revalidated |
revalidateTag, revalidatePath
|
| Full Route Cache | Server, persistent | Until revalidated |
revalidatePath, redeploy |
| Router Cache | Browser, per session | Short TTL or navigation |
router.refresh(), full reload |
You need to know all four. Knowing only one is what causes the "I called revalidatePath and nothing happened" bugs.
Request Memoization vs Data Cache: why they're different
These two are the most commonly confused.
Request Memoization is automatic. If you call the same fetch() URL twice during a single server render, Next.js deduplicates it. The second call returns the already fetched result without hitting the network. It's scoped to a single request and disappears when the render is done. You cannot configure it, disable it, or persist it. It just works.
// Both of these calls in the same render only hit the network once
const user = await fetch('/api/user/123')
const userAgain = await fetch('/api/user/123') // deduped automatically
Data Cache is different. It persists across requests. When you do a fetch with caching:
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
})
That cached result survives across multiple users and multiple requests until the 60-second TTL expires or you explicitly call revalidateTag(). This is the cache you need to think about for interal data.
The key difference: Request Memoization handles the "same fetch called twice in one render" problem. Data Cache handles the "this data should not hit the database on every request" problem. They are solving different things.
Full Route Cache: when your page stops being static
The Full Route Cache stores the rendered HTML and React Server Component payload for a page. Next.js generates this at build time for static routes and serves it to every visitor without rerunning the server component.
Your page is static (cached in Full Route Cache) when:
- It has no dynamic functions (
cookies(),headers(),searchParams) - All its fetch calls use
cache: 'force-cache'or arevalidatevalue
Your page opts out of Full Route Cache when:
- You use
cookies()orheaders()inside a server component - You set
export const dynamic = 'force-dynamic' - A fetch call inside it uses
cache: 'no-store'
One dynamic fetch in a layout component can silently make the entire route dynamic. This is a footgun. I've seen production apps serving a fully static marketing page do a database round trip on every request because a single deeply nested component called cookies() for analytics.
revalidatePath() and redeployment both invalidate the Full Route Cache. A revalidateTag() call only invalidates it if the route's fetch calls are tagged with the same tag.
Router Cache: the client side cache that surprises everyone
The Router Cache is the one that trips up nearly everyone who calls revalidatePath() and wonders why the page hasn't updated.
It lives in the browser, not the server. Next.js caches prefetched and visited route segments in memory for the duration of the user's session. This means even after you invalidate the Data Cache and Full Route Cache on the server, the user may still see stale content because their browser is serving the cached client side payload.
Two things clear it:
-
router.refresh()called in a client component - A full browser page reload
'use client'
import { useRouter } from 'next/navigation'
function SubmitButton() {
const router = useRouter()
async function handleSubmit() {
await updateData()
router.refresh() // clears Router Cache for current route, triggers revalidation
}
return <button onClick={handleSubmit}>Submit</button>
}
Important: revalidatePath() called in a server action does not clear the Router Cache in the same request. (Next.js revalidatePath docs) You still need router.refresh() on the client if you want an immediate update.
On demand revalidation with revalidatePath and revalidateTag
Time based revalidation (the revalidate: 60 pattern) is fine for data that can be a minute stale. For data that must update immediately after a mutation, you want on demand revalidation.
revalidatePath(path) invalidates the Full Route Cache and Data Cache for all fetch calls associated with that path. Use it after mutations that affect a whole page.
revalidateTag(tag) is more surgical. Tag your fetch calls, then invalidate only what changed.
// Tag the fetch at data-fetch time
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// Invalidate only posts-tagged data after a mutation
import { revalidateTag } from 'next/cache'
async function createPost(data: FormData) {
'use server'
await savePost(data)
revalidateTag('posts') // only posts-tagged fetches are invalidated
}
For unstable_cache (the server side equivalent of SWR, useful when you're caching database queries or other data that does not go through fetch), invalidation works the same way with revalidateTag. (Next.js unstable_cache docs)
import { unstable_cache } from 'next/cache'
const getProducts = unstable_cache(
async () => db.query.products.findMany(),
['products'],
{ tags: ['products'] }
)
Next.js 15 caching changes: the breaking change you need to know
If you migrated from Next.js 14 to 15 and your data stopped being cached, this is why.
In Next.js 14, fetch() requests were cached by default. The default was cache: 'force-cache'. You had to explicitly opt out with cache: 'no-store'.
In Next.js 15, fetch requests are not cached by default. (Next.js 15 release blog) The default is now cache: 'no-store'. You have to explicitly opt in with cache: 'force-cache' or a revalidate value.
This is a breaking change with no deprecation warning during migration.
// Next.js 14 default — cached
const data = await fetch('https://api.example.com/data')
// Next.js 15 default — NOT cached (same code, different behavior)
const data = await fetch('https://api.example.com/data')
// To opt into caching in Next.js 15
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// or
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache'
})
The migration path: audit every fetch call in your app and add explicit caching configuration where you actually want it. Rely on the default only for data you intentionally want fresh on every request.
FAQ
How does caching work in Next.js App Router?
Next.js App Router uses four caching layers: Request Memoization (deduplicates fetch calls within a single server render), Data Cache (persists fetch results across requests until revalidated), Full Route Cache (stores rendered HTML for static routes), and Router Cache (browser memory cache for prefetched route segments). Each layer has its own invalidation mechanism.
How do I disable caching in Next.js 14?
To opt a specific fetch call out of caching, add { cache: 'no-store' } as the second argument: fetch(url, { cache: 'no-store' }). To make an entire route always dynamic, add export const dynamic = 'force-dynamic' at the top of the route file. Note that in Next.js 15, fetches are not cached by default so you may not need to do anything if you have already upgraded.
What is the difference between fetch cache and route segment config in Next.js?
The fetch cache (cache option on fetch() or next.revalidate) controls the Data Cache for that specific data fetch. Route segment config (export const dynamic, export const revalidate) sets the default behavior for the entire route. Route segment config is a blunt instrument. Individual fetch options are more precise and composable.
What does router.refresh() do?
It triggers a soft navigation to the current route, clearing the Router Cache for the current route and refetching server component data. It does not do a full browser reload. Use it in client components after server mutations when you need the UI to reflect the updated data immediately.
Why is unstable_cache still prefixed with unstable_?
The API was marked unstable while the team iterated on its interface. The behavior is production ready and widely used. The prefix signals that the API shape may still change in a future major version. Use it, but be prepared for a migration if the API changes.
I've wired up Next.js caching for AI products with complex data fetching requirements — the four layer model is especially important when mixing realtime data with statically cached content.
If you want to see a practical example of caching in a Next.js project, I walk through building a calculator in Next.js where fetch caching decisions matter.
Drop a comment if you've hit a caching bug that isn't covered here — curious what edge cases people are running into in production.
Top comments (0)