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.
| Layer | Scope | Lifetime | Invalidated by |
|---|---|---|---|
| Request Memoization | Single RSC render pass | Per request | Automatic (dedupe) |
| Data Cache | Fetch / unstable_cache results | Cross-request, persistent | revalidateTag, revalidate timer |
| Full Route Cache | Prerendered HTML + RSC payload | Cross-request | revalidatePath, redeploy |
| Router Cache | Client RSC payload | Soft navigation session | router.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:
fetchdefaults to cached in the Data Cache. Usecache: 'no-store'ornoStore()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
revalidatePathtoo.
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 sparinglyInterview: 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 navigationCommon 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 priceInterview: 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