skip to content

In Next.js, what is the difference between calling `revalidatePath('/blog')` and `revalidateTag('posts')` from `next/cache`, and what does the tag version buy you that the path version cannot?

level: middleimportance: must knowfreq 72%

answer

  1. two ways to address the same cache
  2. one names URLs, one names data
  3. who has to know the site structure?
  4. one change, many pages
  5. labels declared where the data is read

basics

~20 s

revalidatePath invalidates cached output by route path, so you must know every URL affected by a change. revalidateTag invalidates every cached entry labelled with that tag, wherever it lives, so one call can refresh many routes at once.

solid answer

~50 s

Both are on-demand invalidation APIs exported from `next/cache`, and both mark cached entries invalid rather than re-rendering anything on the spot — the next request for an affected route does the rendering. The difference is the *addressing scheme*. `revalidatePath('/blog')` targets a route by its URL path, so the caller has to enumerate the affected URLs; it also takes an optional second argument, `'page'` or `'layout'`, where `'layout'` cascades to the segments nested beneath it. `revalidateTag('posts')` targets a **label** attached to cached data when it was cached, so a single call fans out to every route whose render consumed anything carrying that tag — including routes the caller has never heard of. That fan-out is the point: when an author is renamed, you do not want to know which two hundred pages embedded that author's name. In current Next.js these functions must be called from a Server Action or a Route Handler, not during a render.

code

typescript · 12 lines
typescript
import { revalidatePath, revalidateTag } from 'next/cache'

export function invalidateByPath(slug: string) {
  revalidatePath(`/blog/${slug}`) // one concrete URL
  revalidatePath('/blog/[slug]', 'page') // every instance of the dynamic route
  revalidatePath('/dashboard', 'layout') // segment plus everything nested under it
}

export function invalidateByTag(authorId: string) {
  revalidateTag('posts') // every cached entry labelled 'posts'
  revalidateTag(`author-${authorId}`) // fans out wherever that author was read
}

go deeper

for a junior

Know that both come from next/cache and both clear cached output on demand: one is addressed by route path, the other by a label attached to the data. Saying which argument each takes is enough at this level.

for a middle

Explain that they invalidate rather than render, and that the tag version fans out to every route whose data carried the tag. Mention the 'page' versus 'layout' argument and where these calls are allowed to run.

for a senior

Show you reason about who owns knowledge of the routing structure, and why a CMS webhook should name the entity that changed rather than enumerate URLs. Talk about diagnosing an invalidation that silently did nothing.

for a principal

Own the tag taxonomy as an interface between the content system and the app: naming conventions, granularity, and how a too-coarse tag turns one edit into a site-wide re-render across your infrastructure.

## Both are invalidation, not regeneration The first thing to get right is that neither function renders anything. They mark cached entries invalid. The work of producing new output happens on the next request that needs the route. So "I called `revalidateTag` and my page did not change" is usually a story about no one having requested the page yet, or about looking at a copy that was never invalidated in the first place. ## `revalidatePath` — addressing by URL ```ts import { revalidatePath } from 'next/cache' revalidatePath('/blog') // that one route revalidatePath('/blog/[slug]', 'page') // every route matching that dynamic pattern revalidatePath('/dashboard', 'layout') // that segment and everything nested under it ``` The second argument declares what kind of segment the first argument names. `'page'` treats it as a leaf route; `'layout'` cascades, invalidating the nested segments beneath it as well. For a dynamic route, passing the literal bracket pattern rather than a concrete URL is how you invalidate all of its instances at once — passing `/blog/my-post` invalidates exactly that one post. Path addressing works well when the mapping from a change to affected URLs is short and obvious: an editor edits post `my-post`, so `/blog/my-post` and probably `/blog` need refreshing. It falls apart when that mapping is large, dynamic, or unknown to the caller. ## `revalidateTag` — addressing by label Tags are attached to cached data at the moment it is cached, and later invalidated by name: ```ts import { revalidateTag } from 'next/cache' revalidateTag('posts') ``` Whatever produced the cached data — a tagged data fetch, or an explicitly cached function — carries a set of string labels. `revalidateTag('posts')` invalidates every entry wearing that label, and by extension every cached route whose render depended on such an entry. This inverts responsibility, which is the real design win. With paths, the *invalidator* must know the routing structure of the site. With tags, the *reader* declares its own dependency at the point of use, and the invalidator only has to name the thing that changed. A publish webhook can say `revalidateTag('posts')` without any knowledge of whether posts appear on the home page, an index, an RSS route, a sitemap, or a sidebar in a layout — all of which quietly refresh anyway. ## Fan-out, concretely Say every fetch that reads author 42's profile is tagged `author-42`, and every fetch that lists posts is tagged `posts`. The author changes their display name. `revalidateTag('author-42')` invalidates their profile page, every article page that showed a byline, and the index cards on the home page — because each of those renders touched data wearing that tag. Doing the same with paths means computing the list of affected URLs at invalidation time, in the webhook, from data the webhook does not have. Teams that go down that road end up shipping a query whose only job is to answer "which URLs did this row appear on?", which is exactly the coupling tags remove. ## Where you may call them In current Next.js these are side-effecting APIs and belong in a Server Action or a Route Handler — a mutation path or a webhook endpoint — not in the body of a component during render. Calling them mid-render is a bug: rendering is supposed to be free of cache side effects, and Next.js treats it as an error. ## Choosing between them Use `revalidatePath` when the change maps to a small, known set of URLs and no tagging discipline exists — it needs no setup and reads obviously. Use `revalidateTag` when one change ripples across pages, when the invalidating code sits outside the app's routing knowledge (a CMS webhook is the canonical case), or when you want the dependency declared next to the data rather than in the invalidator. Most real codebases use both: tags for content entities, paths for the handful of aggregate routes that are easier to name than to label. The cost of tags is naming discipline. Tags are plain strings with no registry and no compiler help: a typo produces no error, just a page that silently never refreshes. Deriving them from a single helper (`postTag(id)`) rather than typing literals at each site is the practice that keeps them honest.

  • Why does `revalidatePath` take a second argument of `'page'` or `'layout'`?
    It declares what the path names, which determines the blast radius. `'page'` invalidates that route as a leaf; `'layout'` invalidates the segment and cascades to everything nested beneath it. It also lets you pass a dynamic route pattern such as `/blog/[slug]` with `'page'` to invalidate every instance of that route at once, rather than one concrete URL.
  • A team calls `revalidateTag('posts')` and nothing changes. What are the likely causes?
    Most often nothing was ever cached under that tag — the tag was never attached, or the string differs by a typo or a plural. Otherwise the invalidation worked and no one has requested the affected route yet, since these APIs invalidate rather than re-render. Checking whether a render actually runs on the next request separates those two cases.
  • Is there a downside to tagging generously, say putting a global 'content' tag on everything?
    Yes — a coarse tag makes every invalidation invalidate the whole site, so one trivial edit forces every route to re-render on its next request. That converts a targeted refresh into a site-wide cold cache and a burst of load on your data sources. Tag granularity should follow the granularity of real change events, not convenience.

saying these in an interview costs you the question

  • Says these functions immediately re-render the affected pages
  • Thinks revalidateTag works without anything having been tagged
  • Believes revalidatePath accepts a wildcard like /blog/*
  • Calls either API from inside a component render
  • Treats tags as free, tagging everything with one global label

context