How it works
The explanation, with examples you can run.
Tagging and invalidating
// lib/posts.tsimport { cacheLife, cacheTag } from "next/cache" export async function getPosts() { "use cache" cacheLife("max") // no reason to refresh on a clock cacheTag("posts") return db.post.findMany({ orderBy: { createdAt: "desc" } })}// app/api/cms-webhook/route.ts: called by the CMS when content changesimport { revalidateTag } from "next/cache" export async function POST(request: Request) { if (request.headers.get("x-webhook-secret") !== process.env.CMS_SECRET) { return new Response("Forbidden", { status: 403 }) } revalidateTag("posts", "max") return Response.json({ revalidated: true })}// app/posts/actions.ts"use server"import { updateTag } from "next/cache"import { redirect } from "next/navigation" export async function createPost(formData: FormData) { const user = await requireUser() // actions are public endpoints const post = await db.post.create({ data: { title: String(formData.get("title")), authorId: user.id } }) updateTag("posts") redirect(`/posts/${post.id}`)}For content that changes on an event (a CMS publish, an admin edit), a long cacheLife plus a tag is better than a short clock: pages stay in the static shell and refresh only when something actually changed.
Stale-while-revalidate or wait
revalidateTag(tag, profile) | updateTag(tag) | revalidatePath(path) | |
|---|---|---|---|
| Called from | Server Actions, Route Handlers | Server Actions only | Server Actions, Route Handlers |
| Next request | served stale while refreshing (with "max") | waits for fresh data | invalidates the route's cached data |
| Use for | background updates, webhooks | the user's own change | when you don't know the tags |
The second argument to revalidateTag is required in Next.js 16: it says how long stale content may still be served. "max" is the recommended choice; { expire: 0 } means never serve stale. The old one-argument form is deprecated.
Two related tools: refresh() in a Server Action re-renders the current page for the client without touching any cache, and revalidatePath with a dynamic pattern ("/blog/[slug]") needs a type, "page" or "layout", to say how much of the tree to invalidate.
None of these purge a CDN that sits in front of your server under its own rules; that is part of how you deploy, covered later in this path.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.