Topics
v5.0.0
On this page · sections

Caching & Revalidation

The App Router maintains four distinct cache layers. Each stores different data, has different lifetimes, and requires different invalidation strategies. Confusing them is a top source of stale-data bugs.

LayerScopeLifetimeInvalidated by
Request MemoizationSingle RSC render passPer requestAutomatic (dedupe)
Data CacheFetch / unstable_cache resultsCross-request, persistentrevalidateTag, revalidate timer
Full Route CachePrerendered HTML + RSC payloadCross-requestrevalidatePath, redeploy
Router CacheClient RSC payloadSoft navigation sessionrouter.refresh(), hard reload

Request Memoization

Definition

During a single RSC render pass, identical fetch calls with the same URL and options are deduplicated automatically. This is not persistent — it lasts only for that one request.

async function getUser(id: string) { return fetch(`https://api.example.com/users/${id}`, { next: { tags: [`user-${id}`] }, }).then((r) => r.json()); } export default async function Page(props: { params: Promise<{ id: string }>; }) { const { id } = await props.params; const [profile, settings] = await Promise.all([ getUser(id), // HTTP call getUser(id), // deduplicated — no second HTTP call ]); return ( <div> {profile.name} — {settings.email} </div> ); }

Interview: Request memoization dedupes within one render. It does not persist across requests — that is the Data Cache.

Data Cache

Definition

The Data Cache stores results of fetch and unstable_cache across requests. This is where ISR, tags, and revalidation timers live.

// Cached by default in Server Components const data = await fetch("https://api.example.com/products", { next: { revalidate: 3600, tags: ["product-list"] }, }); // Opt out — fresh every request const live = await fetch("https://api.example.com/stock", { cache: "no-store", }); // Explicit no-store helper import { unstable_noStore as noStore } from "next/cache"; export async function getSessionData() { noStore(); return fetchSession(); }

Real-world example

unstable_cache wraps non-fetch server functions with the same tag system:

import { unstable_cache } from "next/cache"; export const getFeaturedProducts = unstable_cache( async () => db.product.findMany({ where: { featured: true } }), ["featured-products"], { tags: ["product-list"], revalidate: 300 } );

Interview: fetch defaults to cached in the Data Cache. Use cache: 'no-store' or noStore() to force dynamic behavior.

Full Route Cache

Definition

The Full Route Cache stores prerendered HTML and RSC payload for static and ISR routes. This is what the CDN serves at the edge.

// Static route — cached at build time export const dynamic = "force-static"; export default async function AboutPage() { return <main>About</main>; } // ISR route — cached with revalidation export const revalidate = 60; export default async function BlogPage() { const posts = await fetchPosts(); return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>; } // Dynamic route — not in Full Route Cache export const dynamic = "force-dynamic"; export default async function Dashboard() { const session = await getSession(); return <main>Hello {session.user.name}</main>; }

Invalidated by revalidatePath or redeploy. Does not auto-refresh when Data Cache tags are invalidated.

Interview: Full Route Cache = prerendered HTML/RSC at the edge. Tag invalidation alone does not bust it — use revalidatePath too.

Router Cache

Definition

The Router Cache is a client-side in-memory cache of RSC payloads during soft navigation (Link clicks). It is not the CDN and not persistent across hard reloads.

"use client"; import { useRouter } from "next/navigation"; export function RefreshButton() { const router = useRouter(); return <button onClick={() => router.refresh()}>Refresh data</button>; }

Behavior:

Soft navigation (Link) → may serve cached RSC payload from Router Cache router.refresh() → busts Router Cache, refetches from server Hard reload → bypasses Router Cache entirely Back/forward → may restore from Router Cache (stale-while-revalidate)

Interview: Router Cache is client session memory — not CDN, not Data Cache. Hard reload always bypasses it.

revalidateTag

Definition

Tags are labels attached at fetch time. revalidateTag purges all cached fetch entries with that tag across every route that used them.

// lib/data.ts export async function getProduct(id: string) { const res = await fetch(`https://api.example.com/products/${id}`, { next: { tags: [`product-${id}`], revalidate: 3600 }, }); return res.json(); }
"use server"; import { revalidateTag } from "next/cache"; export async function updateProduct(id: string, data: FormData) { await saveProduct(id, data); await revalidateTag(`product-${id}`); }

Interview: Tags operate on the Data Cache — the fetch result graph, not rendered HTML.

revalidatePath

Definition

revalidatePath invalidates the Full Route Cache for a URL. The next visit regenerates that route’s HTML and RSC payload.

"use server"; import { revalidatePath } from "next/cache"; export async function updateProduct(id: string, data: FormData) { await saveProduct(id, data); revalidatePath(`/products/${id}`); revalidatePath("/products"); // listing may show updated name/price }

Path types:

revalidatePath('/products/1') → page only revalidatePath('/products/1', 'layout') → layout + all nested routes revalidatePath('/', 'layout') → entire app — use sparingly

Interview: Path invalidation is URL-scoped. A layout type invalidates the subtree.

Why you often need both

Definition

Tags purge cached data; paths purge cached rendered output. Stale HTML can still serve old content even after tag invalidation.

Data Cache (fetch / unstable_cache) → revalidateTag('product-1') → next request refetches that data Full Route Cache (prerendered HTML/RSC) → revalidatePath('/products/1') → next visit regenerates that route's HTML
"use server"; import { revalidatePath, revalidateTag } from "next/cache"; export async function updateProduct(id: string, data: FormData) { await saveProduct(id, data); await revalidateTag(`product-${id}`); revalidatePath(`/products/${id}`); revalidatePath("/products"); }

Interview: Tag invalidation does not automatically invalidate rendered HTML. Pair both when data and output must refresh together.

Tag design & cache flow

Definition

Design tags around entities and collections — not arbitrary strings scattered in fetch calls.

// lib/cache-tags.ts export const tags = { product: (id: string) => `product-${id}`, category: (slug: string) => `category-${slug}`, productList: "product-list", navigation: "site-nav", }; export async function getCategoryProducts(slug: string) { return fetch(`https://cms.example.com/categories/${slug}/products`, { next: { tags: [tags.category(slug), tags.productList], revalidate: 600 }, }).then((r) => r.json()); }

Cache flow for a request:

Browser request → Request Memoization (dedupe fetches in this render) → Data Cache (cross-request fetch results) → Full Route Cache (prerendered route HTML/RSC) → Response to browser → Router Cache (client soft navigation cache)

Invalidation map:

revalidateTag → Data Cache revalidatePath → Full Route Cache router.refresh() → Router Cache staleness Redeploy → Full Route Cache (all static routes)

Senior-level usage

After mutations in Server Actions, always:

1. Persist the change (DB / CMS) 2. revalidateTag for affected data 3. revalidatePath for every URL whose HTML depends on that data 4. Return success — RSC payload refreshes on next navigation

Common footguns:

Tag only, no path → data refreshes but HTML may still be stale Path only, no tag → route regenerates but may read stale cached fetch Broad layout purge → revalidatePath('/', 'layout') spikes origin load Missing listing tag → PDP updates but category page shows old price

Interview: Centralize tag strings in a registry. Typos in tag names cause silent stale-cache bugs.

Forcing dynamic behavior

Definition

Opt out of static caching via export const dynamic = 'force-dynamic', fetch(..., { cache: 'no-store' }), noStore(), or dynamic APIs (cookies, headers). Calling cookies() in a cached layout may serve user A’s response to user B — use noStore() for personalized fetches.

Interview: Four layers, four invalidation strategies. Name all four before explaining any one in detail.

Senior Interview Summary

Four layers

Request Memoization (per-render dedupe), Data Cache (persistent fetch), Full Route Cache (prerendered HTML), Router Cache (client soft nav).

revalidateTag

Surgical Data Cache invalidation by label. Cross-route — one tag can affect many pages.

revalidatePath

URL-scoped Full Route Cache invalidation. Regenerates HTML/RSC on next visit.

Best mental model

Request memo → "Dedupe in this render pass." Data Cache → "Persistent fetch results." Route Cache → "Prerendered HTML at CDN." Router Cache → "Client memory on soft nav." Tags → "This data changed." Paths → "This page's output changed."

Practical rule

After mutation → revalidateTag + revalidatePath (both) Entity update → revalidateTag(`entity-${id}`) Listing affected → revalidateTag('entity-list') + revalidatePath('/listing') Client shows old nav → router.refresh() Debug caching → name all four layers first Avoid → revalidatePath('/', 'layout') unless you mean it

Last updated on