Skip to content

Revalidation and Cache Invalidation

Once data is in the Data Cache you invalidate it two ways — time-based (let it go stale after N seconds) or on-demand (revalidateTag / revalidatePath after a mutation) — and both also refresh the Full Route Cache for affected routes.

Give a cached fetch a lifetime with next: { revalidate: N }, or set it for a whole route segment with export const revalidate. After the window passes, the next request triggers a background refresh.

// Per fetch: this data is at most 60 seconds stale
await fetch('https://api.example.com/posts', { next: { revalidate: 60 } })
// app/blog/page.tsx — revalidate the entire segment every hour
export const revalidate = 3600

For data that changes on an event, not a clock, tag the fetch, then call revalidateTag(tag, profile) after the mutation. Every fetch carrying that tag is marked stale in the Data Cache. The second argument is required in Next.js 16 (the one-argument form is deprecated and a TypeScript error): it is a cacheLife profile that says how long stale data may still be served while the refresh runs — 'max' is the recommended value. When the user must see their own write immediately, call updateTag(tag) from the Server Action instead; it expires the entry so the next request waits for fresh data.

lib/posts.ts
export async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})
return res.json()
}
app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify({ title: formData.get('title') }),
})
// Mark every fetch tagged 'posts' stale; 'max' = serve stale while refreshing in the background
revalidateTag('posts', 'max')
}

Both run inside a Server Action or Route Handler. revalidateTag targets data by tag, wherever it is fetched. revalidatePath targets a route and clears everything cached for it — reach for it when you do not know the tags.

'use server'
import { revalidatePath } from 'next/cache'
export async function updateProfile() {
// Re-render and refetch everything for this exact route
revalidatePath('/profile')
}

revalidatePath('/', 'layout'): the blast radius

Section titled “revalidatePath('/', 'layout'): the blast radius”

revalidatePath takes a type parameter, 'page' or 'layout'. Passing '/' with 'layout' is the largest single revalidation call in the API: the bundled docs describe it as purging the Client Cache and invalidating all cached data for revalidation on the next visit — the root layout wraps the entire app, so its blast radius is every route beneath it. Reach for it only for something that genuinely touches everything (a full content reindex, a locale switch), never as a reflex “just to be safe” call after an ordinary mutation.

'use server'
import { revalidatePath } from 'next/cache'
export async function purgeEverything() {
// Invalidates the root layout — every page in the app revalidates on next visit,
// and the Client Cache is purged immediately. The largest revalidatePath call there is.
revalidatePath('/', 'layout')
}

On-demand revalidation from a Route Handler with a secret

Section titled “On-demand revalidation from a Route Handler with a secret”

A Server Action’s revalidation is triggered by the user’s own mutation, but content that changes outside your app — a CMS edit, a webhook from a payment provider — needs a Route Handler that anyone on the internet can technically call. Without a check, that Route Handler is an open invalidation endpoint. The standard fix, straight from the docs’ on-demand revalidation pattern, is a shared secret passed as a query parameter and compared against a server-only environment variable before doing anything:

app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
// Called by a CMS webhook: /api/revalidate?secret=...&tag=posts
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: 'Invalid secret' }, { status: 401 })
}
const tag = request.nextUrl.searchParams.get('tag')
if (!tag) {
return NextResponse.json({ message: 'Missing tag' }, { status: 400 })
}
revalidateTag(tag, 'max')
return NextResponse.json({ revalidated: true, tag, now: Date.now() })
}

The CMS (or any other webhook sender) is configured with the URL plus the secret; REVALIDATE_SECRET lives only in the server’s environment, never in a NEXT_PUBLIC_ variable, so it never reaches the client bundle. Anyone calling the endpoint without the right secret gets a 401 and nothing revalidates.

Does the x-nextjs-cache header still exist in Next.js 16?

Section titled “Does the x-nextjs-cache header still exist in Next.js 16?”

Yes — checked with curl -I against a next start server in this course’s probe, on a fully static App Router route, in both caching models:

HTTP/1.1 200 OK
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
Cache-Control: s-maxage=31536000

x-nextjs-cache (values HIT / MISS / STALE / REVALIDATED, documented for the older Pages Router ISR flow) is still sent on a prerendered App Router route in Next.js 16.3.6 — it did not get removed. It now sits alongside two newer headers: x-nextjs-prerender (marks the response as a prerendered/static one) and x-nextjs-stale-time (the Client Cache’s stale window in seconds, 300 here matching the default cacheLife profile). All three showed up identically whether cacheComponents was on or off.

Revalidation touches three of the four layers. It drops the matching entries from the Data Cache, and because a route’s rendered output depends on that data, it also marks the affected Full Route Cache entries stale. The next request re-runs the render with fresh data and repopulates both. The browser-side Client Cache is invalidated too: called from a Server Action, revalidateTag / revalidatePath / updateTag purge it immediately; called from a Route Handler, the client picks up the change on its next navigation. Only Request Memoization is untouched — it lives and dies inside a single render.

flowchart LR
  A["Server Action: mutate"] --> B["revalidateTag('posts')"]
  B --> C["Data Cache: drop tagged entries"]
  C --> D["Full Route Cache: mark routes stale"]
  D --> E["Next request re-renders with fresh data"]
  B --> F["Client Cache: purged when called from a Server Action"]
A mutation invalidating the Data and Route caches

Every revalidation call answers the same question — “which cached entries are now wrong?” — with a different granularity. revalidateTag answers it in terms of what data (every entry carrying that tag, wherever it lives). revalidatePath answers it in terms of what route (everything cached for that path, regardless of tags). Time-based revalidate answers it in terms of how long (a clock, not an event). None of them delete data immediately by default — revalidateTag/revalidatePath mark it stale and refresh in the background; only updateTag (Server Actions only) expires it right away for a read-your-own-write.

What you getBenefitCost
revalidateTag(tag, 'max')Precise — only entries carrying that exact tag are touchedRequires the fetch to have been tagged in the first place; a forgotten tag is invisible to this call
revalidatePath('/blog/1')Works without knowing any tagsOnly revalidates that literal path — a sibling page using the same underlying data stays stale until its own path (or the shared tag) is revalidated
revalidatePath('/', 'layout')One call, whole app freshThe biggest hammer available — purges the Client Cache and marks every route for revalidation, which is wasteful for an ordinary single-record mutation
Secret-gated Route HandlerLets an external system (CMS, webhook) trigger revalidation safelyAdds an operational surface: the secret must be provisioned, kept out of NEXT_PUBLIC_, and rotated if it leaks
  • “revalidateTag deletes the cached data immediately.” It does not (by design) — it marks entries stale and serves the old value while a background refresh runs, for up to the cacheLife profile passed as the second argument. Only updateTag expires immediately, and only from a Server Action.
  • “revalidatePath(‘/blog/1’) also revalidates /blog/2 if they share a fetch tag.” It does not — revalidatePath only touches the literal path (or the page/layout pattern given), never other paths that happen to use the same tagged data. That is exactly what tag-based revalidation is for.
  • “revalidatePath(’/’, ‘layout’) is a safe default to sprinkle after any mutation.” It works, but it is the single largest revalidation call available — it purges the Client Cache and marks the entire app for revalidation. Reach for the narrowest call that is actually correct.
  • “A revalidation Route Handler without a secret check is fine because the URL is obscure.” An obscure URL is not authorization — anyone who finds or guesses it can trigger revalidation (or, worse, a denial-of-service via repeated cache purges) with no secret in place.

When a generated snippet revalidates cached data, check for these before trusting it:

  • Does revalidateTag get exactly two arguments? A one-argument call is a TypeScript error in Next.js 16 (revalidateTag requires a cacheLife profile as the second argument) — a generated snippet written against older docs or training data commonly omits it.
  • Is a revalidation Route Handler missing a secret check? Any Route Handler that calls revalidateTag/revalidatePath needs to verify a caller-supplied secret against a server-only environment variable before doing anything.
  • Is revalidatePath('/', 'layout') used for an ordinary single-record mutation? That is the largest possible blast radius — reach for a tag or a specific path instead unless the change genuinely affects the whole app.
  • Does the code call updateTag from a Route Handler? updateTag only works inside Server Actions — a Route Handler needs revalidateTag instead.

🐛 Spot the bug

'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify({ title: formData.get('title') }),
})
revalidateTag('posts')
}
Show the answer

revalidateTag is called with only one argument. Since Next.js 16 it requires a second argument — a cacheLife profile describing how long stale data may still be served while the refresh runs. Proven in the probe: tsc --noEmit on this exact call reports error TS2554: Expected 2 arguments, but got 1. — a real compile error, not a lint warning. The fix is revalidateTag('posts', 'max') (or another named profile), which compiles cleanly.

What are the two ways to do time-based revalidation?
Where may you call revalidateTag?
How does revalidatePath differ from revalidateTag?
What must a fetch have before revalidateTag can invalidate it?
Does this compile in Next.js 16?
'use server'
import { revalidateTag } from 'next/cache'

export async function createPost(formData: FormData) {
  await fetch('https://api.example.com/posts', {
    method: 'POST',
    body: JSON.stringify({ title: formData.get('title') }),
  })
  revalidateTag('posts')
}
A single comment was edited. What is the actual blast radius of this revalidatePath call?
'use server'
import { revalidatePath } from 'next/cache'

export async function tweakOneComment() {
  await updateComment()
  revalidatePath('/', 'layout')
}