Next.js 16 guide

Next.js Cache Debugging on Vercel

Classify the cache layer, read the Vercel cache headers, prove the response is wrong, and apply the smallest recovery that restores Acme Shop without a risky broad purge.

Published
Updated
Reading time
12 min read
On this page

"The cache is stale" can mean an old deployment, a cached public HTTP response, a tagged Next.js fetch, an optimized image, or data that was never cached. The safe response is not to purge first. It is to identify the layer, preserve a harmless body marker, and choose a recovery that matches the evidence.

At the scale a managed edge-orchestration layer runs — many routes and regions, several caching mechanisms stacked in front of one origin — that discipline has to be repeatable, not improvised. This guide treats Next.js cache debugging as a fixed decision flow with evidence at every step, not a set of purge commands reached for under pressure.

Use only public test data

Debug Acme Shop with a public catalog URL and a harmless revision marker. Do not put authorization headers, cookies, customer URLs, deployment-protection secrets, or full private payloads into shell history, tickets, or shared logs.

Overview

Outcome and prerequisites

Outcome: You can distinguish a Vercel CDN response state from a Next.js data-cache result, capture a header-and-body transcript, and recover a public catalog issue without touching account traffic.

Prerequisites: A linked Vercel project, the Acme Shop preview or production deployment URL, a known public catalog revision, access to non-sensitive runtime logs, and an approved known-good deployment for rollback.

Follow the Next.js cache debugging decision flow

Use one URL and one expected value. For this scenario, Acme Shop expects /api/public-catalog to contain catalogRevision: "2026-07-14T10:00Z". A cache status without that body check is only a clue.

Figure 1. Cache-debugging decision flow for Acme Shop
  1. Name the expected public revision

    Use a harmless catalog marker, not a customer value.

  2. Capture headers and body twice

    A cache status alone does not prove correctness.

  3. Confirm the deployment

    Restore or promote a known-good deployment if it does not contain the intended release.

  4. Classify CDN versus tagged data

    Correct the response policy for CDN issues; correct source, tags, or paths for data issues.

  5. Read the status reason, not just the state

    A cold miss, a collapsed request, and a broken cache key all show MISS but need different fixes.

  6. Validate or recover narrowly

    Record a correct body as evidence; invalidate only a reviewed public tag after the response is fixed.

Do not purge when the body is already correct. Treat a private-data exposure as a data-isolation incident, not a cache-tuning exercise.

One decision flow, one observability surface

Acme Shop's catalog may sit behind Vercel today and behind additional regional providers tomorrow. This decision flow stays identical either way; only where you look changes. MYO exists so Optimi can correlate a Vercel x-vercel-cache transcript with the rest of the edge stack in one view, instead of a separate login and header vocabulary per provider during an incident.

Capture Vercel cache headers before changing anything

Request the same public URL twice. -D - prints response headers and the default output prints the body, producing one shareable transcript without a private credential.

curl -sS -D - https://acme-shop-preview.example/api/public-catalog
curl -sS -D - https://acme-shop-preview.example/api/public-catalog
Representative output - second public request. HIT, MISS, STALE, PRERENDER, REVALIDATED, and BYPASS are Vercel CDN states; the revision in the body decides whether the result is correct.
HTTP/2 200
content-type: application/json
cache-control: public, max-age=0, must-revalidate
x-vercel-cache: HIT
x-vercel-id: cle1::iad1::abc123

{"catalogRevision":"2026-07-14T10:00Z","products":[{"slug":"solar-pack","name":"Solar Pack","priceCents":12900}]}

For a protected preview, vercel curl (CLI v48.8.0+) and vercel httpstat (v48.9.0+, plus a local httpstat install) avoid placing a bypass secret in the command. Both are beta, so keep ordinary curl as the portable capture.

vercel curl /api/public-catalog --deployment https://acme-shop-preview.example
vercel httpstat /api/public-catalog --deployment https://acme-shop-preview.example

Read the cache-control headers the function actually sent

curl -D - only shows what reaches the client. Vercel has three related cache-control headers, and one never leaves the edge:

// Route handler: three targeted directives, three different audiences.
return new Response(JSON.stringify(catalog), {
  headers: {
    "content-type": "application/json",
    "Cache-Control": "public, max-age=0",
    "CDN-Cache-Control": "public, s-maxage=60",
    "Vercel-CDN-Cache-Control": "public, s-maxage=3600, stale-while-revalidate=300",
  },
})

Vercel-CDN-Cache-Control controls only Vercel's own edge; it is never forwarded to the browser or another CDN. CDN-Cache-Control is returned to the browser and downstream CDNs. If a function omits CDN-Cache-Control, Vercel strips s-maxage and any background-refresh directive from the cache-control the browser sees, substituting public, max-age=0, must-revalidate — exactly the header in the transcript above. A missing s-maxage in a curl capture is expected, not proof the route is uncached; trust x-vercel-cache and the body over the browser-facing cache-control value.

Identify the stale layer behind cache HIT MISS states

SymptomLikely layerFirst evidence to collect
The current response lacks the intended commit or release behaviorDeployment or build outputvercel inspect <deployment-url> and the body revision
A complete public JSON response is old and x-vercel-cache reports HIT or STALEVercel CDNHeaders, body marker, cache tag, and response eligibility
Several pages show an old catalog valueNext.js data cacheExact fetch tag, source record, webhook result, and runtime logs
An image remains old after its source changedImage optimization cacheSource-image URL and its specific invalidation workflow
Only signed-in visitors see itPrivate data path or application logicA controlled test account and origin/application logs, not a shared-cache change

x-vercel-cache reports the CDN response state. It can be MISS for a dynamic route even when a server-side data fetch was cached. A harmless body revision and application logs are the evidence for the data layer.

Every cache HIT MISS states pair also carries a reason, which Vercel surfaces next to the header in the request's runtime log "Cache" section, and which changes what to do next:

StateReason worth checkingWhat it means for Acme Shop
MISSColdFirst request after a deployment, or an evicted rarely-hit entry. Expected once, not a bug.
MISSRequest collapsedA traffic spike sent identical requests; Vercel ran one origin fetch and held the rest. Not a caching defect.
STALETag-based invalidationA webhook invalidated acme-shop:catalog-response; this entry serves once more before its background refresh.
REVALIDATEDTag-based deletionThe entry was hard-deleted (dangerously-delete, or revalidateTag with no lifetime), so this request paid full origin latency instead of serving stale-then-refresh.
A CDN miss can still use cached Next.js data

Read the response body and Vercel CDN state together: x-vercel-cache describes the response cache, not whether the server-side fetch was cached.

Download:PNGSVG

Inspect the active deployment and contract

Confirm the deployment serving traffic before changing a cache rule. Then inspect the exact public response and the fetch that supplies it.

vercel inspect https://acme-shop-production.example
vercel logs --environment production --query "public-catalog" --since 1h --expand
// Public data: intentional persistence, fallback freshness, known invalidation target.
await fetch("https://catalog.acme-shop.example/v1/products", {
  cache: "force-cache",
  next: { revalidate: 300, tags: ["acme-shop:catalog"] },
})

// Private data: never make this a shared response.
await fetch("https://accounts.acme-shop.example/v1/me", {
  cache: "no-store",
  headers: { Authorization: `Bearer ${accessToken}` },
})

The fetch pattern above is the Next.js data cache, and it keeps working in Next.js 16 as long as the project has not opted into cacheComponents. If Acme Shop later enables cacheComponents: true and moves the catalog function to "use cache: remote" with cacheTag/cacheLife, that data moves into Vercel's Runtime Cache instead — check the project's Runtime Cache observability panel (reads, writes, hit rate, revalidations by tag) alongside logs, since a webhook success no longer guarantees the tagged entry refreshed in that region.

Check for Set-Cookie, request Authorization, private, no-cache, no-store, Vary: *, redirects, unsupported status codes, and body differences — any of these explains why a public response is not cacheable, and none should be bypassed to improve a metric. A response only becomes eligible for Vercel's CDN cache when: the request is GET/HEAD with no Range or Authorization header; the status is 200, 404, 410, 301, 302, 307, or 308; the body is under 10MB (20MB streaming); and the response has no Set-Cookie, no Vary: *, and no private/no-cache/no-store directive. A response that fails any one of these is correctly uncached — a contract issue to fix in the route, not a cache layer to purge.

Use the narrowest recovery

  1. Wrong deployment: restore the approved known-good deployment, then repeat the same body check.
  2. Wrong source or Next.js tagged data: correct the source and invoke Acme Shop's authenticated, allowlisted revalidation endpoint for its known tag and paths.
  3. Wrong public CDN response: fix the route's output or headers first. Once correct, invalidate only the response's CDN tag from a reviewed linked-project session.
  4. Optimized image: invalidate the specific source image; do not purge catalog or account responses.
# Only after the public response is correct and the tag scope is reviewed.
vercel cache invalidate --tag acme-shop:catalog-response

# Multiple related tags in one call, comma-separated, no spaces.
vercel cache invalidate --tag acme-shop:catalog-response,acme-shop:catalog-listing

# Image optimization keys off the source image URL, not the page tag.
vercel cache invalidate --srcimg /images/products/solar-pack.png

# Use only when immediate service restoration requires the approved prior release.
vercel rollback https://acme-shop-known-good.example

vercel cache invalidate --tag marks that tag stale: the next request serves the stale body instantly and refreshes it in the background, at no latency cost to the visitor — the narrow, default choice. dangerously-delete and a project-wide vercel cache purge --type cdn --yes behave differently on purpose: they delete the entry (or the whole project), so the next matching request blocks on a foreground fetch, and simultaneous requests for the same deleted entry can stampede the origin. Reserve them for when a stale response is worse than a slow one, and scope with --tag or --srcimg rather than a project-wide purge.

Prove recovery and record it

ValidationExpected resultFailure signal and response
PositiveThe next public request has the expected revision; a later request remains correct and may change from STALE to HIT.Revision stays old: confirm source data, tag spelling, and affected path before trying another invalidation.
Negative/account is still dynamic/private and no public route contains a test account marker.Any cross-account marker: remove shared eligibility, roll back if needed, and treat it as a data-isolation incident.
FailureA controlled upstream error is visible as an error response or application error, not a fabricated fresh catalog.A cached success masks the controlled failure: inspect CDN TTL and upstream error handling before restoring cache behavior.

Keep the incident record small but complete: URL, expected and observed revision, timestamps, region header, deployment, command scope, operator, and final validation. Attach the runtime log's "Cache" section line for the validating request rather than retyping the reason. Never record secrets or private bodies.

Troubleshooting

SymptomCauseNarrow recovery
x-vercel-cache: HIT but the body is wrongA valid cached response contains an old or bad source representation.Verify the source revision, correct it, then invalidate only acme-shop:catalog-response.
x-vercel-cache: MISS but the catalog still looks oldThe CDN missed while the Next.js data cache supplied the fetch result.Inspect the fetch tag and webhook logs; do not increase CDN TTL.
The browser response does not show s-maxageThe function set Cache-Control without CDN-Cache-Control, so Vercel strips shared directives before forwarding to the browser.Use x-vercel-cache plus the body and route source as evidence, not the browser-facing cache-control value.
Preview works but production is oldThe production alias points at another deployment or production environment differs.Run vercel inspect for the production deployment and compare revision/configuration.
Two deployment URLs return different results for the same requestThe cache key includes the unique deployment URL, so each deployment starts with its own cold cache.Compare through the production alias, and expect one cold MISS per deployment.
A burst of MISS right after an invalidation, then it settlesConcurrent requests during background regeneration can collapse into one origin call; protective, not broken.Confirm the body is correct once the burst settles; escalate only if it stays wrong.
vercel cache invalidate cannot find the expected resultThe response was not emitted with that tag, the project is not linked, or the scope is wrong.Verify the tag in source and linked project; use the webhook for Next.js data tags.
An image stays stale after invalidating the catalog tagImage optimization caches by source image URL, not by the page's response tag.Use vercel cache invalidate --srcimg <source-image-url> instead.
Local development never reproduces productionDevelopment fetch/HMR behavior differs from deployed cache behavior.Reproduce in a preview deployment with the same public URL and body marker.

Authoritative references

Turn cache HIT MISS states into a repeatable runbook

Optimi orchestrates Performance, Security, and Visibility across your edge stack, with MYO giving one observability view for Vercel cache headers and every other provider your team relies on. Talk to us about cache runbooks and recovery paths for critical Next.js workloads.

Discuss cache observability