A team's Next.js App Router codebase ends every Server Action with revalidatePath('/', 'layout'). Reads to the upstream API have grown far faster than traffic. Explain what that call invalidates, why it produces this symptom, and how you would narrow it without reintroducing stale data.
answer
- layout means everything nested below
- root layout is the whole app
- reads now scale with writes, not traffic
- proportional invalidation
- per-entity tags, migrate one action at a time
basics
~20 sPassing the root path with 'layout' invalidates that layout and every route nested under it — effectively the whole app — so one unrelated write discards every cached read and the next navigations all refetch. Narrow it with per-entity tags or specific paths.
solid answer
~50 s`revalidatePath('/', 'layout')` names the root layout and everything nested beneath it, so it is the app-wide sledgehammer: after any write, every cached route render and every cached fetch behind them is stale, and the following navigations all go back to the upstream API. That is why reads scale with *mutations* rather than with traffic, and it also erodes the client Router Cache, so navigation feels slower right after any user action. The fix is to make invalidation proportional to what actually changed. Tag reads by entity — `['posts', `post-${id}`]` — and have each action invalidate only the tags it touched; use `revalidatePath` with a specific route, or the `'page'` type, where the data is not read through the tagged layer. Then verify per action rather than trusting the pattern: the reason teams reach for the sledgehammer is usually that they cannot see which reads a page depends on.
code
typescript · 16 lines'use server'
import { revalidateTag, revalidatePath } from 'next/cache'
// Before: every write nukes the whole app
// revalidatePath('/', 'layout')
// After: name only what changed
export async function updatePost(id: string, formData: FormData) {
await fetch(`https://api.example.com/posts/${id}`, {
method: 'PATCH',
body: formData,
})
revalidateTag(`post-${id}`)
revalidatePath('/sitemap.xml')
}go deeper
Know that revalidatePath takes an optional 'page' or 'layout' argument, and that using 'layout' at the root path invalidates the whole application rather than one route.
Explain the blast radius concretely: which cached things go stale, that the refetch happens on the next visit, and how per-entity tags let one action invalidate exactly what it changed.
Connect the pattern to the observed metric — reads tracking write volume, slower navigation right after user actions — and lay out a staged migration that narrows invalidation while proving nothing went stale.
Own the dial between over-invalidation and staleness as a product decision per surface, plus the conventions that keep it from drifting: where tags are defined, how mutations declare their effects, and what monitoring makes a regression visible.
## What that call means `revalidatePath` takes an optional second argument, `'page'` or `'layout'`. `'page'` invalidates the specific route; `'layout'` invalidates that layout segment *and every route nested under it*. Passing `'/'` with `'layout'` therefore names the root layout — which every route in the app is nested under. It is documented as the way to invalidate everything, and that is exactly what it does. Run it at the end of every action and you have coupled the entire cache's lifetime to the frequency of unrelated writes. ## Why upstream reads outgrow traffic Caching converts N requests into one upstream read. Invalidating everything on every mutation breaks that ratio in a specific, measurable way: - Every cached `fetch` result behind every route is stale, so the next render of each route refetches — including data no user changed. - Statically rendered routes lose their cached render and are produced again. - The client Router Cache is expired too, so a user's next `<Link>` navigation goes to the server instead of being instant. In a busy app, mutations are frequent enough that few cache entries live long enough to be reused, so read volume converges on "once per navigation" — the pattern the cache existed to avoid. The tell in the metrics is exactly the one described: upstream reads correlate with write volume rather than with page views, and p95 navigation time is worst immediately after any user interaction. ## Narrowing without regressing The goal is invalidation proportional to the change. **1. Establish what each write actually affects.** For each action, list the entities it mutates. This is the step teams skip; the sledgehammer is usually a symptom of not knowing the read dependencies. **2. Tag reads by entity, at the read site.** ```ts export async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`, { next: { tags: ['posts', `post-${id}`] }, }) return res.json() } ``` Collection tag plus per-entity tag is the workhorse pattern: editing one post invalidates that post everywhere it is rendered, and the collection tag only when membership or ordering changed. **3. Invalidate only those tags in the action.** ```ts 'use server' import { revalidateTag } from 'next/cache' export async function updatePost(id: string, data: FormData) { await save(id, data) revalidateTag(`post-${id}`) } ``` **4. Keep paths for the route-shaped remainder.** Some routes read outside the tagged layer — a direct database query, a generated feed, a sitemap. `revalidatePath('/sitemap.xml')` or `revalidatePath('/posts/[slug]', 'page')` covers those precisely. The dynamic-pattern form is the right tool when a change genuinely affects all instances of a parameterised route. **5. Migrate action by action, watching the numbers.** Replace the blanket call in one action, confirm upstream reads drop and nothing goes stale, then move on. A big-bang removal across dozens of actions is how the stale-data bugs that motivated the sledgehammer come back. ## The judgment part Over-invalidation and stale data are two ends of one dial, and the honest answer is that they are not equally bad everywhere. A published-price surface may deserve heavy-handed invalidation; a comment count does not. Decide per surface, and prefer a per-entity tag over a coarse one wherever the entity is identifiable. Also keep the invalidation next to the write. When invalidation lives in UI code or in a shared wrapper, nobody can tell what a given mutation affects, and the next person under time pressure reaches for the root-layout call again — which is how these codebases end up here in the first place. ## What you should be able to defend Why `'layout'` at `'/'` is app-wide rather than "just the shell"; why the symptom is read volume tracking writes; a concrete narrowing plan with tags; and a rollout that verifies rather than assumes. Saying only "use tags instead" without explaining the blast-radius mechanism is the weak version of this answer.
- Is revalidatePath('/', 'layout') ever the right call?Occasionally — after a deploy-shaped change that really does affect everything, such as a global config or content-model change applied out of band, or as a deliberate escape hatch in an admin tool. The defect is using it as the default ending for ordinary per-entity mutations, where the blast radius is known and small.
- How would you tell whether narrowing has left something stale?Enumerate each action's mutated entities and each route's read dependencies, then check them off — tags make this tractable because the dependency is declared at the read. Back it with monitoring: upstream read volume should fall, and stale reports should not appear. Migrate one action at a time so any regression has an obvious cause.
- Why does this pattern also hurt perceived navigation speed, not just the API?Invalidation from inside a Server Action also expires the client Router Cache for that session. With an app-wide call, every user action throws away the prefetched payloads for routes they have already visited, so the next Link click hits the network instead of rendering instantly.
saying these in an interview costs you the question
- Thinks 'layout' only invalidates the shared shell, not nested routes
- Believes invalidation is free because it does not refetch immediately
- Replaces every path call with one global tag everyone invalidates
- Blames upstream API scaling rather than the invalidation pattern
- Removes all blanket calls at once with no verification