---
title: "Next.js Cache Debugging on Vercel: Headers, Cache States, and Rollback"
description: "A production-safe Next.js cache debugging workflow to classify Acme Shop cache behavior, read Vercel cache headers and cache HIT MISS states, and recover with the narrowest Vercel action."
canonical_url: https://optimi.com/en/guides/nextjs-vercel-cache-debugging
md_url: https://optimi.com/en/guides/nextjs-vercel-cache-debugging.md
last_updated: 2026-07-15
---

# 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.

"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.

## 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.

```bash
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`](https://github.com/reorx/httpstat) install) avoid placing a bypass secret in the command. Both are beta, so keep ordinary `curl` as the portable capture.

```bash
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:

```ts
// 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

| Symptom | Likely layer | First evidence to collect |
| --- | --- | --- |
| The current response lacks the intended commit or release behavior | Deployment or build output | `vercel inspect <deployment-url>` and the body revision |
| A complete public JSON response is old and `x-vercel-cache` reports `HIT` or `STALE` | Vercel CDN | Headers, body marker, cache tag, and response eligibility |
| Several pages show an old catalog value | Next.js data cache | Exact `fetch` tag, source record, webhook result, and runtime logs |
| An image remains old after its source changed | Image optimization cache | Source-image URL and its specific invalidation workflow |
| Only signed-in visitors see it | Private data path or application logic | A 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:

| State | Reason worth checking | What it means for Acme Shop |
| --- | --- | --- |
| `MISS` | Cold | First request after a deployment, or an evicted rarely-hit entry. Expected once, not a bug. |
| `MISS` | Request collapsed | A traffic spike sent identical requests; Vercel ran one origin fetch and held the rest. Not a caching defect. |
| `STALE` | Tag-based invalidation | A webhook invalidated `acme-shop:catalog-response`; this entry serves once more before its background refresh. |
| `REVALIDATED` | Tag-based deletion | The 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**

![A sequence diagram showing an operator requesting a public catalog through the Vercel CDN. A CDN cache hit returns a cached body immediately; on a CDN miss, the route reads the Next.js data cache, which can itself return tagged catalog data without an origin fetch or fetch the catalog API when absent or stale.](/diagrams/nextjs-vercel-cache-debugging/cdn-and-data-cache-evidence.svg)

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

## 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.

```bash
vercel inspect https://acme-shop-production.example
vercel logs --environment production --query "public-catalog" --since 1h --expand
```

```ts
// 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.

```bash
# 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

| Validation | Expected result | Failure signal and response |
| --- | --- | --- |
| Positive | The 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. |
| Failure | A 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

| Symptom | Cause | Narrow recovery |
| --- | --- | --- |
| `x-vercel-cache: HIT` but the body is wrong | A 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 old | The 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-maxage` | The 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 old | The 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 request | The 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 settles | Concurrent 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 result | The 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 tag | Image 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 production | Development fetch/HMR behavior differs from deployed cache behavior. | Reproduce in a preview deployment with the same public URL and body marker. |

## Related guides

- [Model public and private cache boundaries](/en/guides/nextjs-vercel-cache-model)
- [Revalidate public Next.js data on Vercel](/en/guides/nextjs-vercel-revalidation)

## Authoritative references

- [Vercel: diagnosing cache issues](https://vercel.com/docs/caching/cdn-cache/debug-cache-issues)
- [Vercel CDN Cache](https://vercel.com/docs/caching/cdn-cache)
- [Vercel response headers](https://vercel.com/docs/headers/response-headers)
- [Vercel cache status and reasons](https://vercel.com/docs/caching/cache-status)
- [Vercel: purging the CDN cache](https://vercel.com/docs/caching/cdn-cache/purge)
- [Next.js `revalidateTag`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag)

[Discuss cache observability](/en/contact): 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.
