skip to content

In a Next.js App Router app, a Server Action can finish a write by calling either revalidatePath or revalidateTag from 'next/cache'. What does each one key on, how does data get associated with a tag, and how do you decide which to call?

level: middleimportance: must knowfreq 60%

answer

  1. two coordinate systems for the same caches
  2. one names a route, one names data
  3. tags are attached where you read
  4. page or layout widens the route form
  5. who knows the blast radius

basics

~20 s

revalidatePath keys on a route path and invalidates what that route cached; revalidateTag keys on a label you attached to the reads themselves, for example fetch(url, { next: { tags: ['posts'] } }). Use paths when you know the affected route, tags when the same data appears on many routes.

solid answer

~50 s

They invalidate the same caches but address them differently. `revalidatePath('/posts')` names a **route**: everything cached for that route is marked stale, and you can pass a second argument, `'page'` or `'layout'`, to control whether nested segments go with it. `revalidateTag('posts')` names a **label on the data**: you attach tags where you read — `fetch(url, { next: { tags: ['posts'] } })` — and one call invalidates every cached read carrying that tag, no matter which routes render it. So the decision is about who knows the blast radius. If the mutation clearly affects one or two known routes, a path is simplest and most readable. If a piece of data is rendered by a layout, a dashboard and three detail pages, tagging keeps the action from having to enumerate them, and it keeps working when someone adds a fourth page.

code

typescript · 7 lines
typescript
// lib/posts.ts — tag the read
export async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { tags: ['posts', `post-${id}`] },
  })
  return res.json()
}

go deeper

for a junior

Know that both come from 'next/cache' and are called after a write. Be able to say that one takes a route path and the other takes a label you put on your data fetch.

for a middle

Explain the addressing difference and show the tagging syntax on a fetch, including the optional 'page' or 'layout' argument to the path form and what it widens.

for a senior

Justify the choice from blast radius and maintenance cost: which approach keeps working when a new page starts rendering the same data, and how per-entity tags avoid flushing a whole collection on every small write.

for a principal

Own the tag taxonomy as an API: naming rules, where tagged reads live, how mutations discover which tags to invalidate, and how you stop the scheme from degrading into one global tag everyone invalidates.

## Two ways to name what went stale After a mutation you have to tell Next.js which cached things are now wrong. The two APIs differ only in the coordinate system they use. - `revalidatePath(path, type?)` addresses a **route**. You are saying "whatever is cached for this URL is out of date". - `revalidateTag(tag)` addresses **data**. You are saying "every cached read labelled `posts` is out of date", and Next works out which routes that touches. Both are imported from `next/cache`, both are called from a Server Action or Route Handler, and both mark entries stale rather than refetching on the spot. ## How a tag gets attached Tags do not exist until you put them on a read. The common form is the `fetch` option: ```ts export async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] }, }) return res.json() } ``` A single read can carry several tags, and tags are just strings, which is what makes per-entity granularity possible: `['posts', `post-${id}`]` lets an action invalidate one item without flushing the collection. The practical consequence is that tagging is a *design decision made at the read site*, not at the write site. If nobody tagged the reads, `revalidateTag` has nothing to match, and a team that discovers this late usually finds itself scattering `revalidatePath` calls instead. ## The second argument to revalidatePath `revalidatePath` takes an optional type: `'page'` or `'layout'`. ```ts revalidatePath('/posts') // that route revalidatePath('/posts/[slug]', 'page') // every route matching the dynamic segment revalidatePath('/dashboard', 'layout') // that layout and everything nested under it ``` The dynamic-segment form matters: you pass the *route pattern*, not a rendered URL, when you want to invalidate all instances of a parameterised route. The `'layout'` form widens the blast radius to nested segments, which is powerful and easy to over-use. ## Choosing between them Ask who knows the blast radius. **The action knows the routes.** A settings form that only affects `/settings` should just call `revalidatePath('/settings')`. Reaching for tags here adds indirection for nothing. **The data is spread out.** A post title appears in a sidebar rendered by a layout, on `/posts`, on `/posts/[slug]`, and in a search results page. With paths, every write site has to know that list — and stays correct only until someone adds a new page. With a `posts` tag on the read, one `revalidateTag('posts')` covers all of them, and new consumers are covered automatically because they reuse the tagged read. **You need per-entity precision.** Tags win again: `revalidateTag(`post-${id}`)` invalidates one item's reads instead of everything on a route. **The data is not read through a taggable cached read.** If a route reads directly from a database client rather than through a cached, tagged function, there is no tag to invalidate, and a path is what you have. ## Where they behave the same Both mark entries stale rather than triggering an immediate render; the refetch happens on the next request for an affected route. Both, called inside a Server Action, also expire the client Router Cache for that session, so the user who performed the write does not navigate straight back into a cached pre-mutation payload. Neither pushes anything to other users' open tabs — those clients pick up the change on their next navigation. ## A workable convention Mature codebases converge on: put tagged reads in one data-access module, name tags after domain entities (`posts`, `post-${id}`, `user-${id}`), and let mutations invalidate tags. Keep `revalidatePath` for the genuinely route-shaped cases — a marketing page, a sitemap, a route whose content is not fetched through the tagged layer. The failure mode of the opposite habit is a write path that quietly stops covering half the app the moment someone adds a page. ## What an interviewer is listening for That you can state the addressing difference in one sentence, that you know tags are attached at the read and not conjured at the write, that you mention the `'page'`/`'layout'` argument, and that your choice is justified by blast radius rather than by taste.

  • If nobody tagged the reads, what does revalidateTag actually invalidate?
    Nothing. Tags only exist on reads that declared them, so the call matches no cached entry and the page stays stale. Tagging is a decision made at the read site; the write site can only invalidate labels that already exist, which is why teams centralise tagged reads in a data-access module.
  • Why would you pass a route pattern like '/posts/[slug]' to revalidatePath instead of a concrete URL?
    The pattern form invalidates every route matching that dynamic segment rather than one rendered URL. It is what you want after a change that affects many items — a schema or template change, say — while a concrete path such as '/posts/hello' is right when exactly one item changed.
  • Can one action call both?
    Yes, and it is common. Invalidate the tags for the entity you changed, and add a path for a route that renders the data outside the tagged read layer — a route that queries the database directly, or a generated feed. There is no ordering constraint between them; both simply record invalidations.

saying these in an interview costs you the question

  • Thinks tags exist automatically without being declared on a read
  • Says revalidateTag invalidates a route by name
  • Believes only one of the two can be called per action
  • Assumes revalidatePath('/posts/[slug]') needs a rendered URL
  • Picks between them by preference rather than blast radius

context