Home JavaScript Next.js 16 Caching — Full Route Cache Served 3-Hour-Old Pricing Data on Black Friday
Intermediate 5 min · July 12, 2026

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.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 15 min
  • React
  • Node.js 18+
  • Next.js basics
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
What is The Next.js 16 Caching Model?

Next.js 16 caching is a four-layer system that controls how pages and data are stored across the build step, request lifecycle, render pass, and browser memory. The Full Route Cache (Layer 1) is the most aggressive — it pre-renders entire pages at build time into static HTML and RSC payloads.

Next.js 16 caching is like a warehouse with four separate storage rooms.

Static generation writes these files to disk at build time, while ISR (Incremental Static Regeneration) updates them at runtime with a stale-while-revalidate pattern. The Data Cache (Layer 2) stores individual fetch() responses that Next.js extends with cache options like revalidate and tags.

In Next.js 15+, fetch() defaults to no-store (no caching), a breaking change from v14 where force-cache was the default. The Client Router Cache (Layer 3) lives in browser memory and stores RSC payloads for instant back/forward navigation — with hardcoded TTLs of 30 seconds for static routes and 5 minutes for dynamic routes.

React cache() (Layer 4) deduplicates identical function calls within a single server render pass, preventing redundant database queries or API calls when multiple components request the same data.

These layers stack: when a user visits a page, the Router Cache is checked first (instant if cached), then the Full Route Cache (edge/server), then during server rendering the Data Cache handles individual fetch calls while React cache() deduplicates within that render. The stacking creates a staleness amplification problem — if any layer serves stale data, the entire page can serve stale content silently.

The new 'use cache' directive with cacheLife() and cacheTag() provides declarative caching at the component level, replacing the older unstable_cache API with a native React integration.

The key operational insight: each layer requires independent invalidation. A CMS webhook calling revalidatePath() invalidates the Full Route Cache for that path and the Data Cache for tagged fetches, but it does not touch the Client Router Cache on users' browsers.

To fully refresh a page after a data change, you must invalidate server-side caches and also force a client-side router refresh. Teams that don't understand this stacking produce applications that serve stale data without warning — especially during high-traffic events like Black Friday.

Plain-English First

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

Full Route Cache Does Not Check for Updates
Full Route Cache is an infinite cache by default. If your CMS updates content after a build, the cached HTML will not reflect those changes — no error, no warning, no automatic refresh. You must explicitly trigger revalidation or the page stays stale until the next deploy.
Production Insight
Black Friday pricing disaster: the Full Route Cache was built at 9 PM with 3-hour-old data. The CMS updated at midnight. The cache didn't know. Users saw old prices until the team manually triggered revalidation — 47 minutes after the CMS update.
The fastest fix: never use cache: 'force-cache' on any fetch that powers time-sensitive data. Use next: { revalidate: 60 } to get at most 60 seconds of staleness.
Key Takeaway
Full Route Cache is infinite by default — must explicitly revalidate.
Three invalidation methods: redeploy, revalidate: N, on-demand revalidatePath/revalidateTag.
Rule: for time-sensitive data, never rely on Full Route Cache without revalidation.
nextjs-caching-model-explained THECODEFORGE.IO Next.js 16 Cache Layer Stack How caching layers interact and cause staleness Full Route Cache Pre-built HTML | Static page Data Cache fetch() results | Stale data React cache() Single-request dedup Client Router Cache Browser memory | Cached page use cache Directive Component-level cache THECODEFORGE.IO
thecodeforge.io
Nextjs Caching Model Explained

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.

Data Cache = Shelf Life per Ingredient
  • 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
Production Insight
Auditing Data Cache: grep the codebase for 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%.
Rule: every fetch() must have an explicit cache option. No exceptions.
Key Takeaway
fetch() defaults to no-store in Next.js 16 — opt into caching explicitly.
Three Data Cache options: force-cache (permanent), revalidate: N (stale-while-revalidate), no-store (default, fresh).
Use tags for group invalidation — revalidateTag is more precise than revalidatePath.

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-cache-workaround.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ============================================
// Router Cache Workaround — Force Refresh
// ============================================

// ---- Option 1: router.refresh() on navigation ----

import { useEffect } from 'react'
import { useRouter, usePathname } from 'next/navigation'

export function useRouterRefreshOnFocus() {
  const router = useRouter()
  const pathname = usePathname()

  useEffect(() => {
    const handleFocus = () => {
      router.refresh()
    }

    window.addEventListener('focus', handleFocus)
    return () => window.removeEventListener('focus', handleFocus)
  }, [router, pathname])
}

// ---- Option 2: Version-based staleness check ----

import { useRouter } from 'next/navigation'

export function useVersionCheck(version: string) {
  const router = useRouter()

  useEffect(() => {
    const cachedVersion = sessionStorage.getItem('app-version')
    if (cachedVersion && cachedVersion !== version) {
      router.refresh()
    }
    sessionStorage.setItem('app-version', version)
  }, [router, version])
}

// ---- Option 3: Disable Router Cache for specific routes ----

// File: app/black-friday/page.tsx
// Add a cache-control header to force fresh fetch every time

export const dynamic = 'force-dynamic' // Skips Full Route Cache entirely
export const revalidate = 0 // Revalidate on every request
Try it live
Router Cache Is Invisible to Server Monitoring
You won't see Router Cache staleness in server logs, CDN metrics, or Vercel analytics. It exists entirely in the browser. The only way to detect it is to add client-side version checks or observe that some users see different data than others.
Production Insight
The Black Friday team's webhook invalidated server caches instantly. But 73% of active users still saw old prices for up to 5 minutes because their Router Cache was holding the pre-revalidation RSC payload. Router.refresh() on route focus was the fix — but only for users who navigated after the fix deployed.
Lesson: when you invalidate server caches, you must also invalidate client caches. Add a version header or force router.refresh() for active sessions.
Key Takeaway
Client Router Cache holds RSC payloads for 30s (static) or 5min (dynamic) — not configurable in Next.js 16.
Server invalidation does NOT invalidate Router Cache — you must call router.refresh() client-side.
Use version headers or focus-based refresh to force Router Cache invalidation.
Cache Strategy: Static vs Dynamic Pricing Trade-offs for serving pricing data on Black Friday Static Cache (Default) Dynamic Revalidation Data Freshness Stale up to hours Fresh within seconds Performance Fastest (pre-built) Slower (server compute) Cache Invalidation Manual or time-based Automatic on data change Black Friday Risk High (stale pricing) Low (real-time) Implementation Default Next.js behavior revalidate or dynamic fetch THECODEFORGE.IO
thecodeforge.io
Nextjs Caching Model Explained

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().

react-cache-pattern.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ============================================
// React cache() — Correct and Incorrect Usage
// ============================================

import { cache } from 'react'
import { db } from '@/lib/db'

// ---- CORRECT: At module level ----
// One cache instance for the entire render pass

export const getUser = cache(async (id: string) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [id])
  return user
})

// ---- INCORRECT: Inside a component ----
// New cache instance every render — no deduplication

async function BadComponent({ id }: { id: string }) {
  const getUserLocal = cache(async (userId: string) => {
    return db.query('SELECT * FROM users WHERE id = $1', [userId])
  })

  const user = await getUserLocal(id)
  return <div>{user.name}</div>
}

// ---- Usage: multiple callers, one database query ----

// File: app/layout.tsx
export default async function Layout({ children }: { children: React.ReactNode }) {
  const user = await getUser(sessionId)
  return <div><Header user={user} />{children}</div>
}

// File: app/dashboard/page.tsx
export default async function DashboardPage() {
  const user = await getUser(sessionId) // Returns cached result — no second query
  return <div>Welcome, {user.name}</div>
}
Try it live
cache() = Sticky Note, Not a Filing Cabinet
  • 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
Production Insight
During the Black Friday incident, 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.
Rule: when debugging stale data, check whether cache() is silently deduplicating stale fetches. The absence of duplicate log entries doesn't mean data is fresh.
Key Takeaway
React cache() is request-scoped memoization — not a persistent cache.
Module-level definition is required — component-level definition creates a new cache per render.
cache() can mask staleness by silently deduplicating stale fetches.

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.

use-cache-directive.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ============================================
// 'use cache' Directive — Declarative Component Caching
// ============================================

import { cacheLife, cacheTag } from 'next/cache'

// ---- Cache a Server Component with time-based expiration ----

export default async function ProductGrid() {
  'use cache'
  cacheLife('minutes', 5)

  const products = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] },
  })

  const data = await products.json()

  return (
    <div className="grid grid-cols-3 gap-4">
      {data.map((product: any) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

// ---- Cache a function with tag-based invalidation ----

export async function getPricing() {
  'use cache'
  cacheTag('pricing')
  cacheLife('seconds', 30)

  const res = await fetch('https://api.example.com/pricing')
  return res.json()
}

// ---- On-demand invalidation via tag ----

export async function PATCH(request: Request) {
  const { revalidateTag } = await import('next/cache')
  revalidateTag('pricing')
  return Response.json({ revalidated: true })
}
Try it live
'use cache' Only Works in Server Components
The 'use cache' directive is for Server Components and server-side functions only. Placing it in a Client Component or a file without a server directive will throw a build error. Always verify the component is async and runs on the server.
Production Insight
The 'use cache' directive is the most straightforward way to cache expensive components. A team used it on their product recommendation component — cacheLife('hours', 1) — and saw a 40% reduction in database load. The catch: they forgot to add cacheTag for on-demand invalidation. When the recommendation algorithm updated, users saw old recommendations for up to an hour.
Rule: always pair 'use cache' with cacheTag if the underlying data can change independently of a full redeploy.
Key Takeaway
'use cache' caches entire Server Component output declaratively.
Pair with cacheLife for TTL, cacheTag for on-demand invalidation via revalidateTag.
Only works in Server Components — verify the component runs on the server.

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.

invalidate-all-layers.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ============================================
// Multi-Layer Cache Invalidation
// ============================================

// ---- Server: Invalidate Full Route Cache + Data Cache ----

import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret')

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Invalidate Full Route Cache for product pages
  revalidatePath('/products')

  // Invalidate Data Cache for all pricing fetches
  revalidateTag('pricing')

  // Invalidate Data Cache for product catalog
  revalidateTag('products')

  return NextResponse.json({
    revalidated: true,
    layers: ['Full Route Cache', 'Data Cache'],
    note: 'Router Cache still holds old RSC payloads in browsers',
  })
}

// ---- Client: Invalidate Router Cache ----

// Call this after a CMS publish webhook completes
function forceClientRefresh() {
  // router.refresh() clears Router Cache for current route
  // Then triggers a fresh server render
}

// ---- Alternative: Set short cache TTL via headers ----

export async function GET() {
  const data = await fetch('https://api.example.com/pricing', {
    next: {
      revalidate: 1, // 1 second staleness max
      tags: ['pricing'],
    },
  })

  return new Response(data.body, {
    headers: {
      'Cache-Control': 'private, no-cache, no-store, must-revalidate, max-age=0',
      'CDN-Cache-Control': 'public, s-maxage=1',
    },
  })
}
Try it live
Server Invalidation ≠ Client Invalidation
Calling revalidatePath or revalidateTag on the server only affects the Full Route Cache and Data Cache. The Client Router Cache in each user's browser continues serving the old RSC payload until its 30s/5min TTL expires or the user hard-refreshes. Always communicate cache invalidation to the client via router.refresh() or a version header.
Production Insight
The Black Friday team learned this the hard way. They invalidated server caches within seconds of the CMS update. But 73% of active users saw old prices for up to 5 minutes. The fix: they deployed a version-based staleness check that calls router.refresh() when the app version changes.
Rule: always test cache invalidation from a real user's perspective, not just curl. curl bypasses the Router Cache. Your users don't.
Key Takeaway
Four cache layers stack independently — invalidating one does not invalidate the others.
Server invalidation (revalidatePath/Tag) only affects Full Route Cache and Data Cache.
Router Cache requires client-side invalidation — 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.

cache-monitoring.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ============================================
// Cache Staleness Monitoring
// ============================================

// ---- Server: Log cache hit ratio ----

import { NextResponse } from 'next/server'

export async function middleware(request: Request) {
  const response = NextResponse.next()

  // Read cache status after page render
  const cacheStatus = response.headers.get('x-nextjs-cache')

  // Example: track in your observability system
  console.log({
    path: request.url,
    cacheStatus, // 'HIT' | 'STALE' | 'MISS' | undefined
    timestamp: Date.now(),
  })

  return response
}

// ---- Client: Detect Router Cache staleness ----

'use client'

export function CacheTelemetry() {
  if (typeof window !== 'undefined') {
    const perfEntries = performance.getEntriesByType('navigation')
    const navEntry = perfEntries[0] as PerformanceNavigationTiming

    // If transferSize is 0, the page came from Router Cache or bfcache
    const fromCache = navEntry.transferSize === 0

    if (fromCache) {
      // Log to analytics
      fetch('/api/telemetry/cache-hit', {
        method: 'POST',
        body: JSON.stringify({
          path: window.location.pathname,
          timestamp: Date.now(),
        }),
      })
    }
  }

  return null
}
Try it live
Monitor Cache Hit Ratio Changes
A sudden spike in cache hit ratio is often a sign of stale data — especially after a CMS update. Set up an alert that triggers when the cache hit ratio exceeds a threshold within 5 minutes of a content publish event.
Production Insight
The team that experienced the Black Friday incident now runs a cron job that visits every product page as a headless browser after CMS updates. It compares the rendered price with the CMS price. Mismatches trigger immediate revalidation and a PagerDuty alert.
Rule: treat cache staleness as a production incident, just like a server outage. Monitor it, alert on it, and have a runbook for it.
Key Takeaway
Proactively monitor cache staleness — don't rely on user complaints.
Track x-nextjs-cache header ratio for Full Route Cache monitoring.
Use client-side telemetry to detect Router Cache staleness.

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.

cache-strategy-by-data-type.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ============================================
// Cache Strategy Decision Framework
// ============================================

/*
Data Type            | Change Frequency | Cache Strategy
-------------------- | ---------------- | --------------------------------------------
Site configuration   | Never            | cache: 'force-cache'
Blog posts           | Daily            | next: { revalidate: 86400 }
Product catalog      | Hourly           | next: { revalidate: 3600, tags: ['catalog'] }
Product pricing      | Minutes          | next: { revalidate: 60, tags: ['pricing'] }
User dashboard       | Real-time        | cache: 'no-store'
Live inventory       | Seconds          | cache: 'no-store' + PPR streaming
Recommendations      | Hourly           | 'use cache' + cacheLife('hours', 1) + cacheTag('recs')
*/

import { cache } from 'react'

// ---- Static config: permanent cache ----

export const getSiteConfig = cache(async () => {
  const res = await fetch('https://api.example.com/config', {
    cache: 'force-cache',
  })
  return res.json()
})

// ---- Pricing: short TTL + tag invalidation ----

export const getProductPrice = cache(async (slug: string) => {
  const res = await fetch(`https://api.example.com/pricing/${slug}`, {
    next: { revalidate: 60, tags: ['pricing'] },
  })
  return res.json()
})

// ---- User data: never cache ----

export const getUserData = cache(async (userId: string) => {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    cache: 'no-store',
  })
  return res.json()
})
Try it live
Start Without Cache, Add It When Needed
The safest approach: start with default no-store on all fetches. Profile actual latency. Add caching only when you can measure a real bottleneck and set explicit TTLs based on business requirements for freshness. Most teams cache too much, too early.
Production Insight
The optimal cache strategy is different for every data source. During the Black Friday incident, the team had a single cache strategy for all fetches — force-cache — because they optimized for speed. They should have used different strategies: catalog data with revalidate: 3600, pricing with revalidate: 60 and tag-based invalidation, and inventory with no-store.
Rule: one cache strategy per data source, not one strategy per page.
Key Takeaway
Different data types need different cache strategies.
Use a decision framework based on change frequency and business cost of staleness.
Start with no-store, add caching incrementally with explicit TTLs.
● Production incidentPOST-MORTEMseverity: high

3-Hour-Old Black Friday Prices Served by All Four Cache Layers

Symptom
Black Friday pricing went live in the CMS at midnight. By 12:05 AM, customer support tickets reported incorrect prices. The CMS showed current prices, but the frontend showed 3-hour-old prices from the 9 PM build. CDN cache hit ratio was 97% — extremely efficient, catastrophically stale.
Assumption
The team assumed calling revalidatePath('/products') from the CMS webhook would instantly refresh all users. They didn't consider that the Client Router Cache on each user's browser would still serve the old RSC payload after server-side revalidation.
Root cause
Four layers of caching stacked: 1) Full Route Cache baked the static HTML at build time (9 PM) with a 3-hour-old product list. 2) Data Cache stored individual fetch responses with cache: 'force-cache' and no revalidate. 3) Client Router Cache cached RSC payloads on each user's browser (5 minute TTL for dynamic routes). 4) React 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.
Fix
1) Changed 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.
Key lesson
  • 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
Production debug guideDiagnose which cache layer is serving stale data and how to fix it5 entries
Symptom · 01
Some users see stale data, others see fresh data
Fix
Check Client Router Cache. The stale users likely have an older RSC payload cached.client-side. Have them open a new tab or call router.refresh(). Reduce Router Cache TTL in next.config.js if needed.
Symptom · 02
All users see stale data after CMS update
Fix
Check server-side caches. Run curl -I and look for x-nextjs-cache header: HIT means Full Route Cache is serving stale. Call revalidatePath() or revalidateTag() from the CMS webhook.
Symptom · 03
Data is fresh on first visit but stale on back-navigation
Fix
Router Cache is serving a cached RSC payload from the previous visit. Call router.refresh() on route focus, or set router cache duration to 0 for the affected routes.
Symptom · 04
Inconsistent data across components on the same page
Fix
Check per-fetch cache options. Some fetches may use no-store (fresh) while others use force-cache (stale). Audit every fetch() call on the page for explicit cache options.
Symptom · 05
revalidatePath worked in staging but not in production
Fix
Check if the webhook request reaches the handler. Verify the revalidation secret matches. Check if a CDN or edge cache is serving the old response before it reaches Next.js.
★ Next.js 16 Cache Debugging Quick ReferenceFast commands for inspecting and invalidating all four cache layers
Page shows old content from a previous build
Immediate action
Check Full Route Cache staleness
Commands
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'
Fix now
Call revalidatePath('/products') from a webhook or route handler — invalidates Full Route Cache for that path
fetch() returns old data despite revalidatePath+
Immediate action
Check Data Cache tags on the fetch
Commands
grep -rn 'fetch(' app/ --include='*.tsx' --include='*.ts' | grep -v 'node_modules'
Check if fetch has next: { tags: [...] } or next: { revalidate: N }
Fix now
Add next: { tags: ['products'] } to fetches and call revalidateTag('products') on data change
User sees old data after browser back button+
Immediate action
Router Cache is holding stale RSC payload
Commands
window.__NEXT_DATA__ ? 'SSR' : 'SPA navigation'
Check if the route is using Router Cache — add export const revalidate = 0 for the route
Fix now
Call router.refresh() or set staleTimes.static = 0 in next.config.js
Same data fetched multiple times in one render+
Immediate action
Check if React cache() wraps the fetch function
Commands
grep -rn 'export const get' app/lib/ --include='*.ts'
grep -rn 'cache(' app/lib/ --include='*.ts'
Fix now
Wrap the data-fetching function with cache() from 'react' at module level
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
router-cache-workaround.tsexport function useRouterRefreshOnFocus() {Layer 3: Client Router Cache
react-cache-pattern.tsexport const getUser = cache(async (id: string) => {Layer 4: React cache()
use-cache-directive.tsexport default async function ProductGrid() {The 'use cache' Directive
invalidate-all-layers.tsexport async function POST(request: NextRequest) {How Cache Layers Stack
cache-monitoring.tsexport async function middleware(request: Request) {Production Monitoring
cache-strategy-by-data-type.ts/*Choosing the Right Cache Strategy Per Data Type

Key takeaways

1
Next.js 16 has four caching layers
Full Route Cache, Data Cache, Client Router Cache, and React cache() — each with different scopes and invalidation mechanics
2
fetch() defaults to cache
'no-store' in Next.js 16 — you must explicitly opt into caching with revalidate or force-cache
3
Server invalidation (revalidatePath/revalidateTag) does not invalidate the Client Router Cache
you must call router.refresh() client-side
4
React cache() is request-scoped deduplication, not a persistent cache
define at module level, never inside components
5
Different data types need different cache strategies
start with no-store, add caching incrementally with explicit TTLs
6
'use cache' with cacheLife and cacheTag is the declarative API for component-level caching in Next.js 16
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Name the four caching layers in Next.js 16 and explain when each is inva...
Q02SENIOR
How would you design a caching strategy for an e-commerce site that has ...
Q03SENIOR
What is the difference between cache: 'force-cache' and next: { revalida...
Q01 of 03SENIOR

Name the four caching layers in Next.js 16 and explain when each is invalidated.

ANSWER
1) Full Route Cache: stores entire HTML at build time — invalidated by redeploy, revalidate: N, or revalidatePath/revalidateTag. 2) Data Cache: stores individual 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Full Route Cache and Data Cache?
02
How do I invalidate the Client Router Cache on all active browsers?
03
Does React cache() persist across requests or users?
04
What happens to cached data when I redeploy?
05
Can I use 'use cache' together with fetch() revalidation?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

5 min read · try the examples if you haven't

Previous
Authentication in Next.js 16: Auth.js, Middleware, and Route Protection
35 / 56 · Next.js
Next
Incremental Static Regeneration (ISR) in Next.js 16