Next.js 16 Caching — Full Route Cache Served 3-Hour-Old Pricing Data on Black Friday
Black Friday prices stayed 3 hours old because Full Route Cache, Data Cache, and Router Cache stacked.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓React
- ✓Node.js 18+
- ✓Next.js basics
- Next.js 16 has 4 caching layers: Full Route Cache, Data Cache, Client Router Cache, and React cache() — each with different invalidation mechanics
- Full Route Cache stores entire rendered HTML at build time — persists until next deploy unless you opt into revalidation
- Data Cache (fetch) defaults to no-store in Next.js 16 — you must explicitly set revalidate or cache: 'force-cache'
- Client Router Cache stores RSC payloads for 30s (static) or 5min (dynamic) — router.refresh() clears it
- 'use cache' directive with cacheLife/cacheTag is the new declarative API for component-level caching
- Biggest mistake: assuming one cache layer handles everything — stale data passes through all 4 layers silently
Next.js 16 caching is like a warehouse with four separate storage rooms. The Full Route Cache is the shipping dock with pre-packed boxes — instant to ship, but if the price catalog changes, those boxes still show old prices. The Data Cache is the shelf where individual ingredients are stored — by default, the system fetches fresh ingredients for every order unless you tell it to keep some on the shelf. The Client Router Cache is the customer's phone screen — it remembers what they last saw and doesn't bother asking the warehouse again for 30 seconds. React cache() is the chef's notepad during a single order — they write down the first fetch result so they don't call the supplier twice for the same ingredient. Black Friday fails happen when all four rooms serve stale data because no one told any of them to check for updates.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Next.js 16 ships with a four-layer caching architecture that catches teams off guard when pricing data goes stale during high-traffic events. Each layer operates independently — Full Route Cache at build time, Data Cache at request time, Client Router Cache in the browser, and React cache() within a single render pass. A page can be fully cached by all four layers simultaneously, and you must invalidate each one separately.
The dangerous pattern: an e-commerce page uses static generation with force-cached fetches (Full Route Cache + Data Cache). The CMS updates prices. The build doesn't redeploy. The client still sees the old RSC payload in their Router Cache. Three layers of stale data, zero warnings.
In this article, you'll learn exactly what each layer caches, how to inspect whether it's serving stale data, and the invalidation commands that fix each one. We'll walk through the Black Friday incident that cost $47,000 in price-matching losses before the team understood all four layers.
Layer 1: Full Route Cache — The Pre-Built Box That Never Checks Its Contents
Full Route Cache is the outermost caching layer in Next.js 16. It stores the entire rendered HTML and RSC payload at build time. When a user requests a statically generated route, Next.js serves the cached HTML directly — no server-side rendering, no data fetching, no React component execution. This is the fastest possible response, measured in sub-10ms TTFB from the edge.
A route enters the Full Route Cache when all data dependencies are cacheable (no cookies(), no headers(), no searchParams, and all fetch() calls opt into caching). The cache is keyed by route path and is infinite by default — it never revalidates unless you explicitly configure it.
There are exactly three ways to invalidate Full Route Cache: a new build (redeploy), time-based revalidation via revalidate: N on any fetch or the page itself, or on-demand revalidation via revalidatePath() or revalidateTag(). Without one of these, the cached page persists until your next deploy — which for some teams is days or weeks later.
Layer 2: Data Cache — fetch() Defaults Are Not What You Think
The Data Cache stores individual fetch() responses for Server Components. In Next.js 15+, fetch() defaults to cache: 'no-store' — fresh data on every request. This is a breaking change from Next.js 13/14, which defaulted to cache: 'force-cache'. Many teams upgrading from v14 to v16 missed this change and are now over-fetching, unaware that their data is not cached.
You opt into the Data Cache explicitly. Three options: cache: 'force-cache' (permanent, opt-in, dangerous without revalidation), next: { revalidate: N } (stale-while-revalidate pattern — serve cached, regenerate after N seconds), or next: { tags: [...] } (tagged for group invalidation via revalidateTag).
The Data Cache is independent of the Full Route Cache. You can have a dynamic page (no Full Route Cache) with cached fetch responses. Conversely, you can have a static page (Full Route Cache) where a particular fetch bypasses the Data Cache with no-store and streams fresh data via PPR.
- no-store (default): fetch every request — fresh but slow, no caching overhead
- force-cache: fetch once, cache forever — fastest but must be explicitly invalidated
- revalidate: N: serve stale, regenerate in background after N seconds — best balance
- tags: group fetches for batch invalidation — revalidateTag('products') invalidates all tagged fetches
fetch() calls without explicit cache options. In a Next.js 16 upgrade, one team found 47 fetches with no cache option — all defaulting to no-store, hitting the origin 47 times per page load. Added revalidate: 300 to 32 of them. Server costs dropped 60%.fetch() must have an explicit cache option. No exceptions.Layer 3: Client Router Cache — The Browser Memory That Ignores Server Invalidation
The Client Router Cache stores RSC (React Server Components) payloads in the browser's memory. When a user navigates between pages using the Next.js Link component or router.push(), the Router Cache intercepts the navigation and serves the cached RSC payload — no server request needed, instant navigation.
The Router Cache has two TTLs: 30 seconds for static routes (pre-rendered, no dynamic dependencies), and 5 minutes for dynamic routes (rendered per request). These TTLs are hardcoded in Next.js — you cannot change them via configuration as of Next.js 16. You can work around this by calling router.refresh() or by using cache-control headers on the response.
This is the layer that caught the Black Friday team off guard. They invalidated the server caches (Full Route Cache and Data Cache) via webhook. But each user's browser still held the old RSC payload in Router Cache. Users who clicked navigation links saw stale data for up to 5 minutes. Only users who hard-refreshed or opened new tabs saw the correct prices.
Router.refresh() on route focus was the fix — but only for users who navigated after the fix deployed.router.refresh() for active sessions.router.refresh() client-side.Layer 4: React cache() — Single-Request Deduplication, Not a Cache Cache
React cache() is often misunderstood as a caching layer. It is not persistent — it does not survive across requests, across users, or across builds. cache() deduplicates identical function calls within a single server render pass.
The use case: your layout component and your page component both call getUser(sessionId). Without cache(), this fires two identical network or database requests. With cache(), the second call returns the memoized result from the first call — one request total.
Critical constraint: the cached function must be defined at module level, not inside a component. Defining cache() inside a component creates a new cache instance on every render, meaning zero deduplication. This is the most common bug when adopting cache().
- cache() deduplicates within one render pass — not between requests or users
- Must be at module level — defining inside a component creates a new cache per render
- Pairs with
fetch()options:cache()prevents duplicate work, fetch options control persistence - Use when multiple components on the same page need the same data
cache() was masking the staleness. The layout fetched product pricing at the top level, and the page component called the same cached function — both got the same stale result. cache() was working perfectly, deduplicating stale data. The team didn't notice because the deduplication made the fetch logs look normal.cache() is silently deduplicating stale fetches. The absence of duplicate log entries doesn't mean data is fresh.cache() is request-scoped memoization — not a persistent cache.The 'use cache' Directive — Declarative Component Caching in Next.js 16
Next.js 15+ introduced the 'use cache' directive as a declarative way to cache entire Server Components or async functions at the component level. Unlike fetch() options that control individual API calls, 'use cache' caches the rendered output of a component — HTML, RSC payload, everything.
Pair it with cacheLife() to set a time-based expiration or cacheTag() to enable on-demand invalidation via revalidateTag. This replaces the old unstable_cache API and integrates natively with React's component model.
The directive sits at the top of an async function body. It tells Next.js to cache the result after the first execution, then serve the cached version for subsequent requests within the cacheLife window.
How Cache Layers Stack — And Why You Must Invalidate All Four
The four cache layers compose on a single page. A product page can have: Full Route Cache (build time), Data Cached fetches (request time), React cache() deduplication (render time), and Router Cache (browser). When you update pricing in the CMS, you must invalidate all four — or users see stale data.
The stacking order: Full Route Cache is checked first (edge/server). If it misses or is invalidated, the server renders the page. During rendering, fetch() results come from Data Cache. React cache() deduplicates within that render. The rendered RSC payload is then stored in the client's Router Cache for subsequent navigations.
The critical insight: server invalidation (revalidatePath, revalidateTag) only affects layers 1 and 2. It does not touch layer 3 (Router Cache) or layer 4 (cache() — but that's auto-cleared per request). To force Router Cache invalidation, you must either set aggressive cache-control headers or call router.refresh() client-side.
router.refresh() or a version header.router.refresh() when the app version changes.router.refresh() or version headers.Production Monitoring: How to Detect Stale Cache Before Users Complain
You cannot rely on user support tickets as your stale cache detection method. By the time users report stale prices, the damage is done — lost sales, price-matched losses, regulatory issues. Proactive stale cache detection requires monitoring at every layer.
For Full Route Cache: monitor the x-nextjs-cache response header. HIT means the page was served from cache. Track the ratio of HIT vs MISS vs STALE over time. A sudden increase in HIT rate after a CMS update means data is likely stale.
For Data Cache: audit fetch response times. A sudden drop in fetch latency often means cached data is being served — verify it matches the latest CMS state.
For Router Cache: you cannot detect Router Cache staleness from the server. Add client-side telemetry that logs whether a page was served from Router Cache vs a fresh server fetch.
Choosing the Right Cache Strategy Per Data Type
Not all data needs the same cache strategy. The mistake is applying one cache setting to all fetches. Each data source has different freshness requirements, and your cache configuration should reflect that.
Decision framework: how often does the data change, and what's the business cost of serving stale data? - Site config (never changes): cache: 'force-cache' — permanent, fast, never stale. - Blog posts (daily): revalidate: 86400 — 24 hours of staleness is acceptable. - Product catalog (hourly): revalidate: 3600 with cacheTag for ad-hoc invalidation. - Pricing (minutes): revalidate: 60 with cacheTag('pricing') and webhook-triggered revalidation. - User data (real-time): no-store — never cache user-specific data. - Inventory (seconds): no-store with PPR streaming — fresh per request, but only the inventory component streams.
3-Hour-Old Black Friday Prices Served by All Four Cache Layers
cache() was not the issue but would have deduplicated stale fetches silently. The CMS webhook triggered revalidatePath, which invalidated layers 1 and 2 on the server — but existing users' browsers still held the old RSC payload in Router Cache. Only users who hard-refreshed or opened a new tab saw correct prices.fetch() calls from cache: 'force-cache' to next: { revalidate: 60, tags: ['pricing'] }. 2) Set Cache-Control: no-cache on the API response to respect tag-based invalidation. 3) Added revalidateTag('pricing') to the CMS webhook handler. 4) For existing users, deployed a one-time script that called router.refresh() via a middleware check if the user's cached version timestamp was older than the last CMS update. 5) For future events, added a version header that the Router Cache checks before serving stale RSC.- All four cache layers can stack — invalidating the server does not invalidate the client's Router Cache
- Client Router Cache has a 5-minute TTL for dynamic routes — users see stale data until it expires or they refresh
- Tag-based revalidation with revalidateTag is the most precise invalidation — it targets specific data across layers
- During flash sales, consider reducing Router Cache TTL or adding a version-based staleness check
router.refresh(). Reduce Router Cache TTL in next.config.js if needed.router.refresh() on route focus, or set router cache duration to 0 for the affected routes.fetch() call on the page for explicit cache options.curl -I https://yoursite.com/products | grep -i 'x-nextjs-cache'curl -I https://yoursite.com/products | grep -i 'x-vercel-cache\|cf-cache-status'| File | Command / Code | Purpose |
|---|---|---|
| router-cache-workaround.ts | export function useRouterRefreshOnFocus() { | Layer 3: Client Router Cache |
| react-cache-pattern.ts | export const getUser = cache(async (id: string) => { | Layer 4: React cache() |
| use-cache-directive.ts | export default async function ProductGrid() { | The 'use cache' Directive |
| invalidate-all-layers.ts | export async function POST(request: NextRequest) { | How Cache Layers Stack |
| cache-monitoring.ts | export async function middleware(request: Request) { | Production Monitoring |
| cache-strategy-by-data-type.ts | /* | Choosing the Right Cache Strategy Per Data Type |
Key takeaways
cache() — each with different scopes and invalidation mechanicsrouter.refresh() client-sidecache() is request-scoped deduplication, not a persistent cacheInterview Questions on This Topic
Name the four caching layers in Next.js 16 and explain when each is invalidated.
fetch() responses at request time — invalidated by revalidate: N, revalidateTag, or redeploy. 3) Client Router Cache: stores RSC payloads in the browser — invalidated by router.refresh(), TTL expiry (30s static, 5min dynamic), or hard refresh. 4) React cache(): deduplicates function calls within a single render — automatically cleared after each render. The critical nuance: server invalidation (revalidatePath/Tag) does not invalidate the Client Router Cache.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's Next.js. Mark it forged?
5 min read · try the examples if you haven't