Why Your Next.js App Router Data Is Stale and How to Control Caching
- Author
- Vishal Maurya
- Published on
- Reading time
- 5 min read
Overview
A common App Router symptom: you update a record in your database, reload the page, and still see the old value. In development it looks fine; in production it stays stale. The cause is almost always one of several caching layers, and their defaults changed between Next.js 14 and 15.
This guide explains the layers and shows how to opt in or out deliberately. Always confirm details against the documentation for your installed version.
1. Know the Caching Layers
Several mechanisms can serve you old data:
- Request-level fetch caching: the result of a
fetchcall stored on the server. - Full Route Cache: the rendered output of a static route, built at build time or after revalidation.
- Client Router Cache: an in-browser cache of visited route segments.
Staleness may come from any of them, so identify which one first. Check whether a hard refresh fixes it (client cache) or whether the stale value survives a redeploy-free refresh (server caches).
2. Defaults Differ Between Next.js 14 and 15
In Next.js 14, fetch requests in Server Components were cached by default, and GET Route Handlers were cached when they did not use dynamic functions. The client Router Cache also kept dynamic pages for a short period.
In Next.js 15, these defaults were relaxed: fetch is no longer cached by default, GET Route Handlers are not cached by default, and the client Router Cache no longer reuses page segments on navigation by default. Later versions continue to evolve this area, including newer opt-in caching APIs, so do not rely on memory. Read the caching page for the version in your package.json.
A page can still be static if it uses no dynamic APIs and no uncached data, which means it may be prerendered at build time even when you expected live data.
3. Time-Based Revalidation
If slightly old data is acceptable, set a revalidation interval per request:
// app/products/page.tsx
type Product = { id: string; name: string };
export default async function ProductsPage() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
});
const products: Product[] = await res.json();
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
You can also set a segment-level default with export const revalidate = 60 in a page or layout. With multiple fetches on a page, the lowest interval generally applies to the route, so check how your version resolves this.
4. Force Fresh Data on Every Request
When data must be fresh per request, opt out explicitly:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
const res = await fetch('https://api.example.com/stats', {
cache: 'no-store',
});
const stats = await res.json();
return <pre>{JSON.stringify(stats, null, 2)}</pre>;
}
force-dynamic makes the route render per request. Setting cache: 'no-store' on the fetch makes the intent clear and keeps behavior the same if defaults change. Note that data fetched through an ORM or database client is not a fetch call, so fetch options do not apply to it. Reading cookies() or headers() also makes a route dynamic.
5. On-Demand Revalidation After Writes
Time-based revalidation is a poor fit after a mutation. Tag your data and invalidate it from a Server Action or Route Handler:
// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(id: string, name: string) {
// await db.product.update(...)
revalidateTag('products');
revalidatePath('/products');
}
For this to work, the fetch must carry the tag:
await fetch('https://api.example.com/products', {
next: { tags: ['products'] },
});
The exact signature of revalidateTag has changed in recent releases, so check the reference for your version.
6. Caching Non-Fetch Data
For database calls, wrap the function with unstable_cache (name and status may differ in newer versions) or use the newer caching directives if your version supports them. Give each cached function a stable key and tags, and invalidate with the same tag after writes.
7. Debugging Checklist
- Is the route statically rendered? Check the build output for the static or dynamic marker.
- Does the page read dynamic APIs such as
cookies()? - Is a CDN or reverse proxy in front caching HTML? That is independent of Next.js.
- Does the stale data survive a hard refresh? If not, suspect the client Router Cache.
- Does development behave differently from
next build && next start? Always test caching in production mode.
Conclusion
Stale data in the App Router is rarely a bug; it is a default you have not yet chosen deliberately. Decide per route whether data is static, time-revalidated, tag-invalidated, or fully dynamic, and write that decision in code.
If you are fighting caching behavior in a Next.js application or planning an upgrade between major versions, I can help review the data layer. Contact me.