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 validate stale-while-revalidate behavior without 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. The safe design is allowlisted, authenticated, observable, and preview-tested.
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 event to tags and paths
Tag data by ownership, 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.
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.
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.
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
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. |
| 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 caching and revalidation
- Vercel caching overview
Make freshness predictable
Optimi can help design safe cache tags, webhook authentication, and validation paths for high-traffic publishing workflows.
Discuss cache invalidation