Next.js 16 Caching — Stale Prices from Full Route Cache
Product prices went stale for 24 hours when Full Route Cache baked a force-cached fetch without revalidation.
- Next.js 16 has four server caching layers: Full Route Cache, Partial Prerendering, Data Cache (fetch), and React cache()
- Plus a client-side Router Cache that stores RSC payloads (30s for static, 5min for dynamic)
- Full Route Cache stores entire rendered pages at build time — only when all data is cacheable
- Partial Prerendering splits a page into a static shell + dynamic streaming holes — one strategy per component
- fetch() defaults to cache: 'no-store' in Next.js 15+ — you must opt-in with revalidate or cache: 'force-cache'
- React cache() deduplicates identical fetches within a single render pass — not a persistent cache
- Biggest mistake: assuming fetch() caches by default — in Next.js 16 it doesn't, you must opt in
Next.js 16 caches data and pages at multiple layers. Think of it like a shipping warehouse: Full Route Cache is the pre-packed box ready to ship (fastest, but contents only change when you rebuild or invalidate). Partial Prerendering is like a pre-built picture frame with a live video playing inside — the frame is instant, the content streams. fetch() cache decides how fresh each item should be — by default it fetches fresh every time, you opt-in to keep it. React cache() ensures you don't fetch the same item twice during one packing session. Getting these layers wrong means stale data, slow pages, or both.
Next.js 16 ships with four distinct server caching layers plus a client Router Cache. Each operates at a different level — build time, request time, component render time — and each has different invalidation semantics. Engineers who treat them as interchangeable produce either stale pages or unnecessarily slow ones.
The core change in Next.js 15: fetch() no longer caches by default. In Next.js 13/14, fetch() defaulted to force-cache, causing widespread stale data bugs. In Next.js 16, fetch() defaults to no-store — data is fresh on every request unless you explicitly opt into caching with next: { revalidate } or cache: 'force-cache'.
The correct approach: understand what each layer caches, when it invalidates, and how to opt in selectively. This article breaks down all four layers with production patterns, debugging commands, and the failure scenarios that catch teams off guard.
Layer 1: Full Route Cache (Build-Time Static Generation)
Full Route Cache stores the entire rendered HTML and RSC payload at build time. When a user requests a statically generated page, Next.js serves the cached HTML directly — no server-side rendering, no data fetching, no React rendering. This is the fastest possible response.
Pages enter the Full Route Cache when all data fetching opts into caching (using fetch with cache: 'force-cache' or next: { revalidate: N }) and the page has no dynamic dependencies (no cookies(), headers(), or fetch with no-store). The cache key is the route path.
Invalidation happens in three ways: a new build (redeploy), time-based revalidation (revalidate: N seconds), or on-demand revalidation (revalidatePath() or revalidateTag() called from a route handler or server action). Without explicit revalidation, cached pages persist indefinitely until the next build.
- fetch() does NOT cache by default in Next.js 16 — add next: { revalidate } or cache: 'force-cache' to opt in
- revalidate: N serves stale data immediately, regenerates in background — stale-while-revalidate pattern
- On-demand revalidation via revalidatePath() or revalidateTag() — triggered by webhooks, not timers
- Tagged fetch groups multiple data sources — revalidateTag('products') invalidates all tagged fetches
- generateStaticParams pre-generates known routes — pages are built at build time only if data is cacheable
Layer 2: Partial Prerendering (Static Shell + Dynamic Holes)
Partial Prerendering (PPR) splits a page into a static shell and dynamic streaming holes. The static shell is pre-rendered at build time and served instantly. Dynamic parts are wrapped in Suspense boundaries and streamed in as their data resolves.
PPR is Next.js 16's default. Instead of choosing between fully static and fully dynamic, PPR lets you mix both on the same page. The header, navigation, and layout are static. The personalized content, real-time data, and user-specific elements stream in.
The key constraint: dynamic content must be wrapped in a Suspense boundary AND use uncached data (fetch with no-store, cookies(), headers()). With PPR, a dynamic API only forces that Suspense boundary to stream — it does not make the entire page dynamic.
- Static shell renders at build time — header, nav, layout are cached in Full Route Cache
- Dynamic holes are wrapped in Suspense — they stream in as data resolves at request time
- Fallback UI (skeletons) shows immediately while dynamic content loads — no blank screen
- With PPR, dynamic APIs only force their Suspense boundary to stream — not the entire page
- PPR is default in Next.js 16 — opt out with export const dynamic = 'force-dynamic'
cookies() or searchParams only make that component dynamic, not the whole page.Layer 3: fetch() Cache (Per-Request Data Freshness)
fetch() in Next.js extends the native Fetch API with caching options. In Next.js 15+, each fetch call defaults to cache: 'no-store' — fresh data on every request unless you opt in. You can independently control caching: permanently (force-cache), never (no-store, the default), or with time-based revalidation (revalidate: N).
This is the most granular caching layer. A single page can have five fetch calls with five different cache strategies. Product data might revalidate every 5 minutes. User data might never cache. Static configuration might cache permanently.
The critical nuance: fetch() caching only works in Server Components. In development, every request triggers a fresh fetch even with caching enabled. In production, the fetch result is stored in the Data Cache.
- fetch() without options defaults to no-store in Next.js 16 — data is fresh every request
- cache: 'force-cache' caches permanently — use for static content, add revalidation
- revalidate: N serves stale data immediately, regenerates in background — sweet spot for most content
- Tagged fetch enables group invalidation — revalidateTag('products') invalidates all tagged fetches
- fetch() caching only works in Server Components — client components use native fetch
fetch() that should be cached must have an explicit cache option.Layer 4: React cache() (Request-Level Deduplication)
React cache() deduplicates identical function calls within a single render pass. It is not a persistent cache — it does not survive across requests or builds. It prevents the same data from being fetched multiple times when the same function is called from different components during one server render.
The common scenario: a layout and a page both need the current user. Without cache(), getUser() is called twice. With cache(), the second call returns the memoized result from the first call. The function must be defined at the module level — not inside a component.
- cache() deduplicates identical calls within one server render — not across requests or builds
- Must be defined at module level — defining inside a component creates a new cache per render
- Combines with
fetch()options:cache()for deduplication, fetch options for persistence control - Use when multiple components need the same data — layout + page + sidebar all calling getUser()
cache() deduplicates within one render pass — it is not a persistent cache.cache() inside a component creates a new cache per render — no deduplication happens.cache() is request-scoped memoization — deduplicates identical calls within one server render.cache() creates a new instance per render.cache() with fetch() options — deduplication within request, freshness across requests.Choosing the Right Caching Strategy
The four caching layers are not interchangeable. Each solves a different problem at a different level. Choosing the wrong one produces either stale data or unnecessary server load.
The decision framework: how often does the data change, and how personalized is it? Static config that never changes uses cache: 'force-cache'. Content that changes every few minutes uses fetch() with revalidate. User-specific data uses the default no-store. Multiple components needing the same data add React cache() on top.
- Full Route Cache for the static shell — opt in with cacheable fetches
- PPR Suspense boundaries separate static from dynamic
- fetch() with different revalidate values per data source
- React
cache()deduplicates shared data — category info fetched once
Product prices stale for 24 hours due to Full Route Cache
fetch() would stay fresh by default in Next.js 16, but they had explicitly opted into permanent caching with cache: 'force-cache' for performance.https://api.example.com/products/${slug}, { cache: 'force-cache' }). This opts the data into the Data Cache permanently, and combined with generateStaticParams, the page was fully static at build time. The result was baked into the Full Route Cache. Without revalidate or on-demand invalidation, the cached HTML persisted until the next deploy.fetch() call — this enables stale-while-revalidate every 5 minutes. For prices that change frequently, added on-demand revalidation via revalidateTag('products') triggered by a CMS webhook.- In Next.js 16
fetch()defaults to no-store — you must opt-in to caching with revalidate or cache: 'force-cache' - Full Route Cache bakes cached fetch results into static HTML — stale data persists without revalidation
- CMS-driven content needs webhook-triggered revalidation — do not rely on time-based revalidation alone
fetch() or trigger on-demand revalidation via revalidatePath().fetch() (no-store) or cookies() call is forcing the segment to stream — with PPR only that Suspense boundary streams, not the whole page.cache() — it deduplicates identical calls within a single render pass.fetch() or call revalidatePath() from a webhookKey takeaways
cache() deduplicates within one render passCommon mistakes to avoid
5 patternsOpting into permanent caching without revalidation
Assuming fetch() caches by default
Using dynamic APIs outside Suspense boundaries
cookies(), headers(), and searchParams usage into Suspense-wrapped components.Defining React cache() inside a component
cache() at the module level.Using fetch() caching in Client Components
Interview Questions on This Topic
What are the four server caching layers in Next.js 16, and when does each operate?
cache() operates within a single render — deduplicates calls but doesn't persist.Frequently Asked Questions
That's React.js. Mark it forged?
3 min read · try the examples if you haven't