Next.js caching is the reason a page shows yesterday’s price after you updated the database, and also the reason a well-built App Router site feels instant. This guide walks through all four caching layers, the options that control them, how the defaults changed between Next.js 14, 15 and 16, and a repeatable way to debug stale data.
The four Next.js caching layers at a glance
The App Router does not have “a cache”. It has four separate mechanisms that live in different places and expire for different reasons. Most stale-data bugs come from fixing the wrong one.
| Layer | Where it lives | What it stores | How long |
|---|---|---|---|
| Request memoization | Server, per request | Return values of identical fetch calls during one render |
One render pass |
| Data Cache | Server, persistent | Results of fetch (or 'use cache' functions) across requests and deploys |
Until revalidated |
| Full Route Cache | Server, persistent | Rendered HTML and RSC payload of static routes | Until revalidated or redeployed |
| Router Cache | Browser memory | RSC payloads of visited and prefetched routes | Session, subject to stale times |
Request memoization: deduplicating fetches in one render
During a single server render, React memoizes fetch calls with the same URL and options. If your layout, page and a nested component all call getUser(), the network request happens once. This lets you fetch data where you need it instead of prop-drilling it from the top.
// lib/data.ts
export async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error('Failed to load user');
return res.json();
}
// Called in layout.tsx AND page.tsx: only one HTTP request per render.
Memoization only applies to GET fetches in the component tree and is discarded after the render, so it never causes stale data between requests. For database or SDK calls, wrap the function in React’s cache:
import { cache } from 'react';
import { db } from '@/lib/db';
export const getProduct = cache(async (slug: string) => {
return db.product.findUnique({ where: { slug } });
});
The Data Cache: fetch cache options and revalidate
The Data Cache persists fetch results on the server across requests. You control it per call:
// Cache indefinitely (until revalidated)
await fetch(url, { cache: 'force-cache' });
// Never cache: hit the origin every request
await fetch(url, { cache: 'no-store' });
// Time-based: serve cached, refresh in background after 60s
await fetch(url, { next: { revalidate: 60 } });
// Tag it so you can invalidate on demand later
await fetch(url, { next: { tags: ['products'] } });
Time-based revalidation is stale-while-revalidate: the first request after the window expires still gets old data and triggers a background refresh; the next request gets fresh data.
You can also set a segment default with export const revalidate = 60 in a page.tsx or layout.tsx. With Next.js 16 Cache Components enabled, segment options like revalidate and dynamic give way to the 'use cache' model described below.
The Full Route Cache: static vs dynamic rendering
At build time, Next.js tries to render each route once and store the HTML plus the React Server Component payload. If it succeeds, the route is static and every visitor gets the stored result. The build output marks these routes: ○ for static and ƒ for dynamic (server-rendered on demand).
A static route stays frozen until revalidated or redeployed. The dev server renders every request fresh, while next start serves the Full Route Cache, so always reproduce caching bugs with a production build:
npx next build && npx next start
Dynamic APIs that opt a route out of caching
A route becomes dynamic when rendering it depends on information that only exists at request time. Using any of these switches the route to dynamic rendering:
cookies()andheaders()fromnext/headerssearchParamsin a pageconnection()fromnext/server, ordraftMode()- An uncached fetch, such as
cache: 'no-store' - Route segment config
export const dynamic = 'force-dynamic'(pre-Cache Components projects)
From Next.js 15 these request APIs, along with params and searchParams, are asynchronous and must be awaited:
import { cookies } from 'next/headers';
export default async function AccountPage() {
const cookieStore = await cookies(); // makes this route dynamic
const theme = cookieStore.get('theme')?.value ?? 'light';
const orders = await fetch('https://api.example.com/orders', {
cache: 'no-store',
}).then((r) => r.json());
return <OrdersList theme={theme} orders={orders} />;
}
Dynamic does not mean “nothing cached”. A dynamic route can still read cached fetches from the Data Cache; only the rendered page itself is produced per request.
The Router Cache on the client
The Router Cache lives in the browser. When a user navigates with <Link>, Next.js stores the RSC payload of visited and prefetched segments so that back/forward and repeat navigations are instant. Shared layouts are not re-rendered on navigation.
It is cleared on a full reload, by router.refresh(), or when a Server Action calls revalidatePath/revalidateTag or sets cookies. Mutating through an API route and then navigating client-side can show the cached payload: a Router Cache problem, not a server one. You can tune how long entries stay fresh with the staleTimes option in next.config (introduced as experimental in 14.2; check its status in your version’s docs).
How Next.js caching defaults changed in v14, v15 and v16
Most confusing advice online comes from tutorials written for a different version. Treat this as a summary and confirm details in the upgrade guide for your version.
Next.js 14: cached unless you opt out
fetchdefaulted toforce-cache, so data was cached indefinitely unless you said otherwise.GETRoute Handlers were cached by default.- The Router Cache kept dynamic pages for around 30 seconds and prefetched static pages for several minutes, which made “I navigated back and saw old data” common.
Next.js 15: uncached unless you opt in
fetchis no longer cached by default. You addcache: 'force-cache'ornext.revalidateto opt in. Note that a route can still be prerendered statically if it uses no dynamic APIs; the fetch result then lives inside the Full Route Cache.GETRoute Handlers are dynamic by default;export const dynamic = 'force-static'opts back in.- The Router Cache stale time for page segments defaults to zero, so client navigations fetch fresh page data (layouts and loading states are still reused).
- Request APIs (
cookies,headers,params,searchParams) became async.
Next.js 16: explicit caching with Cache Components
Next.js 16 introduces Cache Components, enabled with cacheComponents: true in next.config. In that mode, data access is dynamic by default and you mark what to cache with the 'use cache' directive, giving it a lifetime with cacheLife and a tag with cacheTag. It also brings Partial Prerendering into the same model: a static shell with dynamic holes wrapped in <Suspense>.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
// lib/products.ts
import { cacheLife, cacheTag } from 'next/cache';
export async function getProducts() {
'use cache';
cacheLife('hours');
cacheTag('products');
const res = await fetch('https://api.example.com/products');
return res.json();
}
Without cacheComponents, a v16 app behaves broadly like v15. The revalidateTag signature also changed: Next.js 16 expects a second argument naming a cache-life profile (for example 'max'), and adds updateTag for Server Actions that need the user to see their own write immediately. These APIs moved quickly across releases, so read the caching docs for the version in your package.json.
On-demand revalidation with revalidatePath and revalidateTag
For anything a user edits, revalidate on demand right after the mutation. revalidatePath purges a route; revalidateTag purges every cached entry carrying a tag, on every route. I prefer tags for shared data, such as a product shown on a listing, a detail page and the homepage.
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function updateProduct(id: string, price: number) {
await db.product.update({ where: { id }, data: { price } });
// Next.js 15: revalidateTag('products')
// Next.js 16: pass a profile, or use updateTag() for read-your-own-writes
revalidateTag('products', 'max');
revalidatePath(`/products/${id}`);
}
For changes that originate outside your app, such as a CMS publish, expose a Route Handler protected by a secret and call revalidateTag from there. If you run a headless CMS, the pattern in headless WordPress with Next.js uses exactly this webhook approach.
Debugging stale data in Next.js
Work through the layers from the outside in:
- Hard reload the page. If fresh data appears, the Router Cache was the culprit. Fix it by revalidating in the Server Action or calling
router.refresh()after client-side mutations. - Check the build output. If the route is marked static (
○), the Full Route Cache is serving it. Add revalidation or make it dynamic. - Log fetches. Enable fetch logging to see cache hits and misses per request.
- Inspect response headers. On self-hosted deployments, the
x-nextjs-cacheheader reportsHIT,MISSorSTALE. - Check the layers outside Next.js. A CDN such as Cloudflare can cache HTML independently of Next.js; see Cloudflare cache rules for how to keep the two aligned.
// next.config.ts
const nextConfig = {
logging: {
fetches: { fullUrl: true },
},
};
export default nextConfig;
Also check tags match exactly: revalidateTag('products') silently ignores a fetch tagged 'product'.
Key takeaways
- Next.js caching is four layers: request memoization, Data Cache, Full Route Cache and the client Router Cache.
- Next.js 14 cached
fetchby default; Next.js 15 does not; Next.js 16 adds opt-in caching with'use cache'under Cache Components. - Dynamic APIs like
cookies(),headers()andsearchParamsmake a route render per request. - Revalidate on demand with tags after mutations instead of shortening revalidate windows.
- Reproduce caching bugs with
next build && next start, never only in dev.
Frequently asked questions
Why does my Next.js page show stale data after I update the database?
The route is most likely static and served from the Full Route Cache, or the fetch is in the Data Cache without revalidation. Call revalidatePath or revalidateTag after the write, or make the route dynamic.
Is fetch cached by default in Next.js 15?
No. Next.js 15 changed the default so fetch is not cached unless you pass cache: 'force-cache' or a next.revalidate value. Next.js 14 cached it by default.
What is the difference between revalidatePath and revalidateTag?
revalidatePath invalidates cached data and rendering for a specific route, while revalidateTag invalidates every cached entry with a given tag, wherever it is used. Tags are better when the same data appears on several pages.
Does caching work the same in next dev?
No. The development server renders routes on every request, so the Full Route Cache never kicks in. Test caching behaviour with a production build using next build and next start.
If your Next.js app is serving stale data or you want a caching strategy designed before launch, get in touch and I can review your setup.