Next.js App Router: the caching layers
- Last updated
- Prerequisites:
- Next.js App Router basics
- Familiarity with fetch and cache-control semantics
- nextjs
- caching
- app-router
- cache
- revalidation
- ssg
Read at your depth
The practical view
The App Router has several caches with distinct lifetimes: the full route cache (prerendered HTML stored on the server/CDN, revalidated by revalidatePath/time-based revalidate); the data cache (persisted fetch() results keyed by URL + options, controlled by next: { revalidate } or fetch's cache); the router cache (client-side cache of visited segments and prefetched payloads, invalidated on navigation to stale-while-revalidate pages); and React's request memoization (dedupes identical fetches within one render pass, always fresh within the request). The practical rule: add noStore()/no-store to anything user-specific, use revalidateTag('x') after mutations, and remember the router cache is per-session on the client.
The same idea in other frameworks
// React 19: <Cache> component, cache() for memoization;
// framework (Next) owns fetch caching and hydration// Angular SSR: HTTP cache-control headers + service worker
// (ngsw) for client caching; no per-route cache pipeline// Nuxt: useFetch keyed caching, swr: true for
// stale-while-revalidate, routeRules for prerender/caching// SvelteKit: adapter-static prerenders everything; full
// static caching by default, no client router cacheLegacy vs modern
Stale user data vs correctly revalidated data
A default fetch caches aggressively and can leak cross-user data; opting the route out and tagging mutations keeps data both fast and correct.
const res = await fetch('https://api.example.com/me');
const me = await res.json();const res = await fetch('https://api.example.com/me', { next: { revalidate: 60 } });Interview gotchas
Context
This is the canonical App Router correctness question — the interviewer wants the caching layers AND the authorization pitfall in one answer.
The mechanical answer
A page with no dynamic signals (no cookies/headers reads, no dynamic config) can be prerendered and its output stored in the full route cache, which may be served to any user. If the data was fetched with the default cache policy, the data cache can also persist a response keyed only by URL+options — not by the auth token — so user B can receive user A's cached payload. The fix is to make the page dynamic (read cookies()/headers(), or set dynamic = 'force-dynamic', or noStore()) and to include the auth token in the fetch key (headers make the cache key distinct) or bypass caching entirely for personal data. Cache tags then scope revalidation to that user's entries.
Trap
The trap answer is 'just add no-store to every fetch'. That sacrifices the pipeline's value and does not fix the full route cache (the HTML can still be cached and served to others). The sharp answer names both layers: per-user pages need dynamic rendering AND per-user cache keys or explicit no-store. Candidates who mention that cache tags + dynamic signals work together ('personalized data is dynamic, public data is cached and tag-revalidated') demonstrate production-grade understanding.
Context
This tests whether the candidate can keep the layers straight — the classic 'explain the caching system' question where memorized answers collapse.
The mechanical answer
The data cache stores fetch() results server-side (keyed by URL and options, with revalidate/time-based and tag-based invalidation). The full route cache stores the rendered HTML of a page on the server/CDN after prerendering, so a request can be served without re-rendering. The router cache lives on the client and stores the Flight payload (segment data + RSC tree) of visited and prefetched routes, enabling instant soft navigation; it is invalidated by navigation and refreshes in the background. The pipeline: router cache (client) → full route cache (CDN/server HTML) → data cache (server fetch results) → origin. Revalidation at any server layer propagates only when the next request actually reaches that layer.
Trap
A common confusion is treating the router cache as part of the CDN cache, or claiming revalidatePath clears the user's browser cache — it cannot; it invalidates server-side entries. Another trap: 'the data cache applies to all fetches' — fetch('...', { cache: 'no-store' }) opts out, and the default is no caching for dynamic APIs in some setups, so blanket statements break down. Naming which layer each invalidation tool touches (revalidateTag → data cache; revalidatePath → full route cache) is what separates a memorized from an understood answer.