Next.js 16 guide
Next.js Caching on Vercel: Safe Defaults for Public Content
Cache one public Acme Shop catalog route deliberately, verify each layer, and keep every personalized response outside shared storage.
On this page
Caching on Vercel is not one switch. A Next.js application can cache a server-side fetch, prerender route output, and let Vercel's CDN cache a complete HTTP response. These are separate layers with separate invalidation behavior. This guide uses Acme Shop's public catalog; it does not cache its account, cart, checkout, or authenticated API responses.
Shared caches require public output
A shared cache can return one stored response to many visitors. Never apply a shared policy to output that depends on a cookie, authorization header, session, entitlement, cart, or another visitor's state. Keep it dynamic and private instead.
Overview
Outcome and prerequisites
Outcome: Acme Shop's public /shop listing uses a five-minute Next.js data-cache policy, while a deliberately public JSON endpoint has a short Vercel CDN policy. You can prove the public route is cacheable and the account route is not.
Prerequisites: A Next.js 16 App Router project that is not using Cache Components, a linked Vercel preview deployment, a public upstream catalog, and a harmless visible revision such as catalogRevision: "2026-07-14T10:00Z". Use preview before production.
Start with Acme Shop's cache boundary
The catalog is identical for all visitors and may be up to five minutes old. An account summary is not identical, even when two visitors request the same URL. Write that distinction down before adding a header or a tag.
- Public visitor
Requests the public catalog representation.
- Vercel CDN
Can cache the complete public GET /api/public-catalog response.
- Next.js Data Cache
Persists the server-side catalog fetch with tag acme-shop:catalog.
- Acme Shop catalog API
Remains the authoritative public product source.
A signed-in visitor takes /account through a no-store fetch to the account API; it never enters a shared cache.
The public response cache and the Next.js data cache can both reduce origin work, but a HIT at one layer is not proof of a hit at the other. Treat the output's privacy contract as the first cache key.
Cache the public catalog fetch explicitly
Next.js 16 does not cache fetch requests by default in this model. Make Acme Shop's intended persistence, freshness, and invalidation target explicit. force-cache permits persistent storage; revalidate limits its lifetime; the tag is reserved for narrow on-demand invalidation.
// lib/acme-shop/catalog.ts
export type Product = { slug: string; name: string; priceCents: number }
export async function getPublicCatalog(): Promise<Product[]> {
const response = await fetch("https://catalog.acme-shop.example/v1/products", {
cache: "force-cache",
next: { revalidate: 300, tags: ["acme-shop:catalog"] },
})
if (!response.ok) throw new Error("Public catalog is unavailable")
return response.json()
}
// app/shop/page.tsx
import { getPublicCatalog } from "@/lib/acme-shop/catalog"
export default async function ShopPage() {
const products = await getPublicCatalog()
return <ul>{products.map((product) => <li key={product.slug}>{product.name}</li>)}</ul>
}
revalidate: 300 is a maximum cache lifetime, not a guarantee that every region warms at the same moment. The smallest revalidation value used by a route can also lower that route's revalidation frequency.
Keep the private account path non-cacheable
Do not try to make a session token part of a shared cache key. Acme Shop fetches account data for the current visitor on every request.
// lib/acme-shop/account.ts
export async function getAccount(accessToken: string) {
const response = await fetch("https://accounts.acme-shop.example/v1/me", {
cache: "no-store",
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!response.ok) throw new Error("Account is unavailable")
return response.json()
}
Reading cookies() or headers() does not make a shared response safe. Ensure the fetch policy, route behavior, and HTTP response headers all match the private-data requirement.
Cache one complete public response at the CDN
The data cache does not automatically make an entire HTTP response CDN-cacheable. For Acme Shop's public catalog API only, return a complete representation with a Vercel-specific TTL and a CDN tag. The browser receives the short browser policy; Vercel consumes Vercel-CDN-Cache-Control and removes Vercel-Cache-Tag before sending the response onward.
// app/api/public-catalog/route.ts
import { getPublicCatalog } from "@/lib/acme-shop/catalog"
export async function GET() {
const products = await getPublicCatalog()
return Response.json(
{ catalogRevision: "2026-07-14T10:00Z", products },
{
headers: {
"Cache-Control": "public, max-age=0, must-revalidate",
"Vercel-CDN-Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
"Vercel-Cache-Tag": "acme-shop:catalog-response",
},
}
)
}
Do not add these headers when the request carries Authorization, the response sets a cookie, output differs by visitor, or the body contains sensitive data. Those responses do not meet Vercel's CDN cacheability criteria.
HTTP/2 200
content-type: application/json
cache-control: public, max-age=0, must-revalidate
x-vercel-cache: HIT
{"catalogRevision":"2026-07-14T10:00Z","products":[{"slug":"solar-pack","name":"Solar Pack","priceCents":12900}]}Validate public, private, and failure behavior
Deploy the reviewed change to a preview URL. For protected previews, vercel curl automatically handles deployment-protection bypass; it is currently a beta CLI command. For a public preview, ordinary curl is sufficient.
vercel curl /api/public-catalog --deployment https://acme-shop-preview.example
vercel curl /api/public-catalog --deployment https://acme-shop-preview.example
vercel httpstat /api/public-catalog --deployment https://acme-shop-preview.example
| Validation | Expected evidence | Stop if |
|---|---|---|
| Positive public route | Two GET /api/public-catalog responses have the expected public revision; the second may report HIT or STALE. | The body contains an account field, a cookie, or a visitor-specific value. |
| Negative private route | An authenticated /account response is fresh for the signed-in test user and has no shared s-maxage policy. | Different test accounts can receive each other's marker or response. |
| Failure behavior | Temporarily use a non-production upstream test that returns 503; the route reports its expected error and does not serve a fabricated success body. | An upstream failure is silently stored as a successful catalog response. |
Vercel's CDN is regional. A warm result from one location is not proof that every region is warm. Save the URL, time, headers, body revision, and request location with the change record.
Recover without broad purges
If a public response is wrong, first remove the unsafe cache eligibility or restore the known-good deployment. Then verify the body and a signed-in journey. Only after the response is correct, invalidate the narrow public CDN tag through a reviewed, linked-project session:
vercel cache invalidate --tag acme-shop:catalog-response
For a wrong Next.js data-cache value, use the authenticated application revalidation workflow in the revalidation guide; a CDN invalidation does not repair bad source data or an unsafe route design. Avoid vercel cache purge as a first response because it is broader than this catalog change and can increase origin load.
Troubleshooting
| Symptom | Likely cause | Narrow check or recovery |
|---|---|---|
x-vercel-cache: MISS on a dynamic page | That header describes the CDN response, not necessarily the server-side fetch. | Add a harmless catalog revision to the body and inspect application logs before changing TTLs. |
Browser Cache-Control lacks s-maxage | Vercel consumes Vercel-only shared directives before forwarding the response. | Inspect x-vercel-cache and the body; do not infer the edge policy from the browser header alone. |
| The catalog never becomes cacheable | The response has Set-Cookie, Authorization, private, no-store, Vary: *, an unsupported method/status, or is too large. | Remove shared eligibility or keep the route dynamic; never work around a privacy restriction. |
| Local development looks stale after an edit | Server Component HMR can retain fetch responses during development. | Navigate or fully reload; validate caching behavior on a deployed preview. |
| A product update is still old | The five-minute fallback has not expired or the publishing event did not revalidate the tag. | Confirm the source record, then invoke the authenticated, allowlisted webhook once. |
Related guides
Authoritative references
- Next.js caching and revalidation
- Next.js
fetchAPI reference - Vercel CDN Cache
- Vercel Cache-Control headers
Make caching a controlled application behavior
Talk to Optimi about cache boundaries, CDN validation, and origin protection for high-traffic Next.js applications.
Discuss cache architecture