Next.js 16 guide
Next.js Revalidation on Vercel: Invalidate Public Data Safely
Map a trusted Acme Shop publishing event to known tags and paths, then drive on-demand ISR with stale-while-revalidate behavior instead of exposing a generic purge endpoint.
On this page
Time-based caching is a useful fallback, but Acme Shop needs its catalog to refresh after a product is published. In Next.js 16's fetch-based caching model, tags attach an invalidation target to cached data. A protected Route Handler can then map one known publishing event to those known targets, turning Next.js revalidation into a deliberate, on-demand ISR operation rather than a generic cache flush. The safe design is allowlisted, authenticated, observable, and preview-tested.
At the scale a managed edge-orchestration layer runs — many origins, many CDNs, events firing around the clock — this discipline keeps "stale content" from quietly becoming "wrong content nobody noticed," whether Acme Shop runs it alone or as one workflow in a broader Performance and Visibility program.
Never expose arbitrary tag or path input
A webhook that lets a caller choose any tag or path is a remote purge endpoint. Authenticate the sender, accept expected event shapes only, and derive the exact tags and paths in server-side application code. Keep this pattern for public content, not account or order data.
Overview
Outcome and prerequisites
Outcome: When Acme Shop publishes solar-pack, its public collection and product fetches are marked stale using revalidateTag(..., "max"); the known collection and product paths are marked for revalidation; the next visits refresh safely in the background.
Prerequisites: A Next.js 16 App Router project not using Cache Components, a public catalog source, a preview deployment, ACME_SHOP_REVALIDATE_SECRET stored only as a server environment variable, and a trusted publisher capable of sending HTTPS POST requests.
Map one Next.js revalidation event to tags and paths
A disciplined Next.js revalidation design starts with ownership: tag data by who is responsible for it, not by whichever component currently displays it. Acme Shop uses one collection tag plus one stable product tag. The webhook maps the known product.published event to literal paths; it never accepts a caller-supplied tag or path.
- Trusted publisher
Sends product.published for the safe slug solar-pack.
- Protected Route Handler
Authenticates and validates the event before it derives any target.
- Catalog tag
revalidateTag(acme-shop:catalog, max) marks collection data stale.
- Product tag and paths
The known product tag plus /shop and /shop/solar-pack are marked for revalidation.
- Next visitor
May receive stale content while fresh data and output regenerate in the background.
The publisher cannot select arbitrary tags or paths. Repeated deliveries safely mark the same public targets stale again.
| Cached data | Tag | Path marked for revalidation | Owner |
|---|---|---|---|
| Public collection | acme-shop:catalog | /shop | Product publishing service |
| One public product | acme-shop:product:solar-pack | /shop/solar-pack | Product publishing service |
revalidateTag applies to every cached use of a tag. revalidatePath applies to a specific route. Use both here because the event changes shared catalog data and two known rendered paths.
The max profile prioritizes availability: invalidation marks entries stale rather than synchronously regenerating every affected page.
revalidatePath also accepts an optional second argument, "page" or "layout", for invalidating an entire dynamic route pattern such as /shop/[slug] in one call rather than looping over literal paths; a bulk reindex tool would use revalidatePath("/shop/[slug]", "page"), while Acme Shop's single-event webhook stays with a literal path and no type argument. The path string must not exceed 1024 characters. If a route is reached through a Next.js rewrite, revalidate the rewrite's destination path, not the source path visitors see — cache entries are keyed to the route file, not the request URL.
Attach the tags to public fetches
Make the data cache policy explicit. The one-hour interval is a fallback if a publishing event fails; it is not a promise that data remains stale for exactly one hour.
// lib/acme-shop/products.ts
export async function getProducts() {
const response = await fetch("https://catalog.acme-shop.example/v1/products", {
cache: "force-cache",
next: { revalidate: 3600, tags: ["acme-shop:catalog"] },
})
if (!response.ok) throw new Error("Products are unavailable")
return response.json()
}
export async function getProduct(slug: string) {
const response = await fetch(`https://catalog.acme-shop.example/v1/products/${slug}`, {
cache: "force-cache",
next: { revalidate: 3600, tags: ["acme-shop:catalog", `acme-shop:product:${slug}`] },
})
if (!response.ok) throw new Error("Product is unavailable")
return response.json()
}
Tags are case-sensitive and each tag must be no more than 256 characters. Do not use this fetch pattern for data that changes by session, authorization, cart, or user entitlement.
next.tags on fetch is one of two ways Next.js 16 attaches tags to cached data. Under Cache Components, the same tag is declared with cacheTag("acme-shop:catalog") inside a 'use cache' function instead. Either path feeds the same revalidateTag primitive covered next; only where the tag is declared changes.
Add an authenticated, allowlisted webhook
The Route Handler rejects missing credentials, malformed JSON, unknown event types, and unsafe slugs before it calls a revalidation primitive. It returns only non-sensitive diagnostic values.
// app/api/revalidate-products/route.ts
import { revalidatePath, revalidateTag } from "next/cache"
import type { NextRequest } from "next/server"
type ProductEvent = { type: "product.published"; slug: string }
function isProductEvent(value: unknown): value is ProductEvent {
if (!value || typeof value !== "object") return false
const event = value as Record<string, unknown>
return event.type === "product.published" &&
typeof event.slug === "string" && /^[a-z0-9-]+$/.test(event.slug)
}
export async function POST(request: NextRequest) {
const expected = process.env.ACME_SHOP_REVALIDATE_SECRET
if (!expected || request.headers.get("authorization") !== `Bearer ${expected}`) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
let body: unknown
try {
body = await request.json()
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 })
}
if (!isProductEvent(body)) {
return Response.json({ error: "Unsupported event" }, { status: 400 })
}
const productTag = `acme-shop:product:${body.slug}`
const productPath = `/shop/${body.slug}`
revalidateTag("acme-shop:catalog", "max")
revalidateTag(productTag, "max")
revalidatePath("/shop")
revalidatePath(productPath)
return Response.json({
revalidated: true,
slug: body.slug,
tags: ["acme-shop:catalog", productTag],
paths: ["/shop", productPath],
})
}
The one-argument revalidateTag(tag) form is deprecated in Next.js 16. With the recommended "max" profile, revalidateTag marks tagged entries stale; it does not eagerly regenerate every page or guarantee the immediately following visitor sees fresh content.
The second argument is not limited to named profiles: revalidateTag(tag, { expire: 0 }) is Next.js's documented pattern for a webhook or third-party integration that needs a tag to expire immediately rather than go through stale-while-revalidate. Acme Shop's catalog tolerates a brief stale window, so this guide keeps "max". A checkout-adjacent price feed that cannot show a stale number to even one visitor is a legitimate reason to swap in { expire: 0 } for that tag — understanding that the trade is a blocking regeneration on the next request instead of an instantly served, still-fresh-enough response.
Understand on-demand ISR's blast radius on Vercel
Before validating behavior, know the platform boundaries this webhook operates inside. They shape what "safe" means once Acme Shop is live:
- Scoped to one domain and deployment. On-demand revalidation on Vercel only affects the domain and deployment where it is triggered. Calling the webhook against the preview URL never touches production, and calling it against production never touches a different subdomain or an older deployment still serving traffic. This is the platform guarantee behind the preview-then-promote flow later in this guide.
- Request collapsing absorbs flash traffic. If
solar-packgoes viral the moment it publishes, Vercel collapses concurrent requests for the same stale-and-regenerating path into one function invocation per region, rather than one per visitor. The webhook only needs to fire once; it does not need to protect the origin from a traffic spike itself. - Failures fail safe. If background regeneration hits a timeout, a function error, or any HTTP status other than
200, 301, 302, 307, 308, 404,or410, Vercel keeps serving the last good cached response and retries after roughly 30 seconds. A200from the webhook means the tag was marked stale, not that the next regeneration attempt is guaranteed to succeed on the first try. - Purges propagate globally, together. Once regeneration succeeds, Vercel purges and replaces the HTML and any associated data payloads across all CDN regions as one atomic push, typically within a few hundred milliseconds — visitors in different regions do not see the update trickle in over minutes.
- Each deployment owns its own ISR cache. A deployment's durable cache is not shared with the next deployment. This matters directly for the rollback path covered below.
Test valid, invalid, and repeated delivery on preview
Set the secret in the preview environment, configure the publisher to call only the preview URL, and use a shell variable rather than putting a secret literal in the command or shell history.
curl -sS -i -X POST https://acme-shop-preview.example/api/revalidate-products \
-H "Authorization: Bearer $ACME_SHOP_REVALIDATE_SECRET" \
-H "Content-Type: application/json" \
--data '{"type":"product.published","slug":"solar-pack"}'
HTTP/2 200
content-type: application/json
{"revalidated":true,"slug":"solar-pack","tags":["acme-shop:catalog","acme-shop:product:solar-pack"],"paths":["/shop","/shop/solar-pack"]}HTTP/2 400
content-type: application/json
{"error":"Unsupported event"}HTTP/2 200
content-type: application/json
{"revalidated":true,"slug":"solar-pack","tags":["acme-shop:catalog","acme-shop:product:solar-pack"],"paths":["/shop","/shop/solar-pack"]}| Validation | Expected evidence | Stop and recover if |
|---|---|---|
| Positive | A valid solar-pack event returns 200 with only the two known tags and two known paths. | The response includes an unexpected tag or path; disable the sender and correct the allowlist. |
| Negative | A missing bearer token returns 401; an unsupported event returns 400; neither response triggers revalidation. | Either request returns 200; remove the endpoint from traffic until authentication and validation are fixed. |
| Failure | With a controlled non-production catalog-source 503, the webhook may return 200 because it only marks entries stale, but the next resource visit must show the route's expected error or fallback rather than a fabricated revision. | A false fresh revision is shown or an error is cached as catalog success; correct source-error handling before promotion. |
Validate stale-while-revalidate timing
Use a public product revision such as productRevision: "18" in preview. Capture the collection and product bodies before publication, publish revision 19, trigger the webhook once, and request each URL twice.
curl -sS https://acme-shop-preview.example/shop
curl -sS https://acme-shop-preview.example/shop/solar-pack
| Moment | Expected behavior with revalidateTag(tag, "max") |
|---|---|
| Before webhook | Cached revision 18 can be served while it remains valid. |
| Immediately after webhook | The tag is marked stale; no large synchronous global refresh begins. |
| First visit to an affected resource | Revision 18 may be served while Next.js fetches and renders revision 19 in the background. |
| Later visit after regeneration | The body should show revision 19. If it does not, inspect the upstream record, tag spelling, and path mapping. |
The timing is intentionally availability-oriented, not read-your-own-write behavior. If the user who made a mutation must immediately see it, use updateTag in a Server Action rather than turning a public webhook into a synchronous global refresh.
Promote and recover safely
After preview passes, add the same secret to production without exposing it to client code, point the trusted sender at the healthy production deployment, and run one controlled update for solar-pack. Monitor webhook failures, upstream errors, revalidation failures, and stale-content reports.
If a bad event or code change reaches production, stop the sender or disable its event first. Correct the source record, restore the approved deployment if necessary, then deliver one known event and validate the collection and product body twice. Do not start with a broad CDN purge: it cannot correct a bad source record or a faulty tag/path map.
# Run only after approval when an immediate deployment rollback is required.
vercel rollback https://acme-shop-known-good.example
Rolling back is fast because the restored deployment reactivates its own ISR cache from when it last served traffic — nothing needs purging. Promoting a brand-new deployment is different: it always starts with a cold ISR cache and cannot inherit the outgoing deployment's warmed entries. Expect the first requests to /shop and /shop/solar-pack after any promotion to regenerate rather than serve an instantly warm response, and budget for that rather than mistaking it for a broken revalidation pipeline.
Treat revalidation as one observable event, not a hope
A 200 from the webhook proves the Route Handler accepted the event — it does not prove the catalog tag went stale everywhere it should have, that the product path actually regenerated, or that a different CDN or edge layer in front of Acme Shop served the refreshed page to a real visitor. A managed orchestration layer such as MYO correlates webhook delivery, revalidation outcomes, and stale-content reports from real traffic under one event, so a partial failure in this pipeline surfaces immediately instead of showing up days later as a support ticket about an old price.
Troubleshooting
| Symptom | Likely cause | Narrow check or recovery |
|---|---|---|
401 Unauthorized | Missing preview/production secret, wrong environment, or malformed bearer header. | Verify the server environment variable and sender configuration; never log the secret. |
400 Unsupported event | Event type, JSON shape, or slug violates the allowlist. | Compare the sender payload with the documented product.published schema and use a safe slug. |
200 but the first page still shows the old revision | "max" deliberately uses stale-while-revalidate. | Request again after regeneration, then verify source data and runtime logs before retrying. |
| Collection refreshes but product page remains old | Product tag or literal product path was omitted or misspelled. | Inspect the returned tags and paths, then correct the server-side mapping. |
| Product refreshes but another catalog surface remains old | That route uses the catalog tag but was not covered by a path-only change. | Revalidate the shared catalog tag and add the known affected route to the mapping if needed. |
| Preview event revalidates but production still shows the old revision | On-demand revalidation is scoped to one domain and deployment; a preview call never reaches production. | Confirm the sender's target URL and re-deliver the event against the intended deployment. |
| Regeneration keeps failing but visitors never see an error | Vercel deliberately serves stale content and retries after roughly 30 seconds when regeneration hits a timeout, bad status, or function error. | Inspect function and runtime logs for the real failure; a healthy-looking webhook response does not mean regeneration succeeded. |
| First page load after a rollback or promotion is noticeably slow | A promoted deployment always starts with a cold ISR cache; it does not inherit the outgoing deployment's warmed entries. | Expect one regeneration-latency hit per path after any deployment change, then re-run the timing validation. |
| A publish-time traffic spike still hits the origin hard | Request collapsing groups concurrent requests to the same path, but unbounded query-string variants or per-visitor URLs fragment that path and defeat collapsing. | Confirm the affected paths are the literal, cacheable ones from the tag/path table, not fragmented by query parameters. |
| Local testing appears inconsistent | Development and HMR fetch behavior differ from a deployed cache. | Test the webhook and public revision marker on a preview deployment. |
Related guides
Authoritative references
- Next.js
revalidateTag - Next.js
revalidatePath - Next.js
updateTag - Next.js caching and revalidation
- Vercel caching overview
- Vercel Incremental Static Regeneration
Make Next.js revalidation predictable at scale
Optimi can help design safe cache tags, on-demand ISR webhooks, and cross-provider validation paths as part of a managed Performance and Visibility program for high-traffic publishing workflows.
Discuss cache invalidation