Skip to main content
sitewarm

Next.js on Vercel: which cache serves a response

· sitewarm

A Next.js page on Vercel can be answered from several places: static storage written at build time, the CDN cache in the region that received the request, a durable ISR cache, or a fresh run of your function. The response header x-vercel-cache says which of these happened. This article explains what Vercel's documentation says each value means, what Next.js adds with Incremental Static Regeneration (ISR), and how to read the headers of your own pages with one command.

Everything here comes from Vercel's and Next.js's documentation, read on 2026-10-25, and from the public sitewarm README. Anything those pages do not state is left out. The example output is an illustrative shape, not captured from a real site.

The layers, as Vercel describes them

Vercel's CDN cache page says the CDN "caches your content (including pages, API responses, and static assets) in data centers around the world" and serves it "from the nearest region". It also says the cache is "segmented by region". Two consequences follow from the same page: later requests for the same path in the same region can be served as a HIT, and a rarely requested entry "may be evicted from the regional cache".

ISR adds a second store. The Vercel ISR page describes a durable ISR cache that "lives alongside your Function region". On a CDN miss, "Vercel reads from the ISR cache before invoking your function". Its request-time diagram has three outcomes: the CDN serves the page and your function does not run, the ISR cache serves it and the copy is replicated back to the CDN, or your function runs and the response is stored.

LayerWhat Vercel's pages sayWhere it shows up
Static storageA response "served from static storage, such as a page prerendered at build time"x-vercel-cache: PRERENDER
CDN cacheSegmented by region. A hit is "served directly from the cache, with no function invocation or origin fetch needed"HIT, or STALE while it refreshes
ISR cacheDurable, next to your Function region, read before the function is invokedNot named by its own header value in the pages read
Your function or originGenerates the response when nothing usable is cachedMISS, BYPASS or REVALIDATED

The third row is a limit of the sources. The response-header page lists six values and none of them is "ISR cache". A response that the ISR cache served and that was replicated back to the CDN is, per the pages read, not given a separate label, so do not infer one.

The six values of x-vercel-cache

The response headers page lists the values below, with the descriptions quoted. The cache status page expands each one and adds a reason that appears in Vercel's runtime logs, not in the header.

ValueWhat the page saysWhat to check next
HIT"the response was served from the cache"Nothing, for this location and this moment.
MISS"the response wasn't in the cache and was generated from your function or origin"Whether the route is meant to be cached. The cache status page says a miss on a dynamic route is expected.
STALE"a cached response was served while Vercel refreshed it in the background"A revalidation window has passed. This is the stale-while-revalidate path.
PRERENDER"the response was served from static storage"The page was prerendered, for example at build time.
REVALIDATED"the response was regenerated in the foreground after the cached entry was deleted"Whether something deleted the entry, such as on-demand revalidation or a purge by tag.
BYPASS"Vercel skipped the cache and served fresh content from your function or origin"Draft Mode, a prerender bypass rule, or a crawler hitting a fallback path, as the cache status page lists.

Two caveats come from the same pages. The response headers page says that for dynamic routes and fetch requests that use the runtime cache, x-vercel-cache "often shows MISS even if the data is served from the runtime cache". And the cache status page says a MISS is stored for later only for ISR and Partial Prerendering pages, "or when the response sets cache-control headers".

What Next.js does for ISR

The Next.js ISR guide gives this sequence for a page with export const revalidate = 60: pages are prerendered during next build; after 60 seconds, "the next request will still return the cached (now stale) page"; the cache is invalidated and a new version "begins generating in the background"; and once that succeeds, the next request gets the updated page. If revalidation throws, "the last successfully generated data will continue to be served from the cache".

On Vercel, the match with the header values is stated by Vercel: the STALE reason "time-based revalidation" is "the standard stale-while-revalidate path, including the revalidation intervals that frameworks like Next.js set through ISR". For on-demand revalidation, the cache status page says an entry deleted through revalidatePath() or revalidateTag() is regenerated in the foreground and shows as REVALIDATED, and that revalidateTag() called with a cache-life profile such as 'max' marks the entry stale instead, so the next request is STALE. The Next.js guide adds that revalidatePath "invalidates the cache entries but regeneration happens on the next request".

The Next.js guide also documents its own header, x-nextjs-cache, with the values HIT, STALE, MISS and REVALIDATED. It is a different header from x-vercel-cache, set by Next.js, and the pages read do not say whether Vercel returns it. Look at what your deployment actually sends.

The Cache-Control a browser sees is not the one the CDN used

Vercel's cache-control page says that when Cache-Control is the only cache header, "Vercel's proxy consumes s-maxage" and the CDN does not include it, or stale-while-revalidate, in the response to the client. The response headers page says that when s-maxage is used to instruct the CDN to cache, Vercel returns cache-control: public, max-age=0, must-revalidate to the client.

So a cache-control header showing max-age=0 does not by itself say the page is uncached on the CDN. Use x-vercel-cache for the CDN, and cache-control for what the browser was told. The page also describes CDN-Cache-Control and Vercel-CDN-Cache-Control: Vercel-CDN-Cache-Control has top priority, then CDN-Cache-Control, then Cache-Control, and Vercel-CDN-Cache-Control is not returned to the browser. For what the standard directives mean, see Cache-Control for HTML pages behind a CDN.

Why a page that should be cached shows MISS

The CDN cache page lists criteria a response must meet to be cached. Among them: a GET or HEAD request; no Range or Authorization header on the request; a 200, 404, 410, 301, 302, 307 or 308 status; no set-cookie header; no private, no-cache or no-store in Cache-Control; and no Vary naming a high-cardinality header such as Cookie. For that last case the page says the response "is returned with x-vercel-cache: MISS" and no entry is written. See why one URL can have two cache entries for how Vary splits entries.

The cache status page also names reasons that are expected rather than faults: a cold entry on the first request, after a new deployment ("Vercel scopes cached responses to the deployment that produced them"), or after eviction; and request collapsing, where several visitors request the same uncached path at once and the held requests "show as a miss".

See the cache state of your own pages

The sitewarm command line reads a sitemap and requests each URL with a desktop and a mobile profile. The README says that for Vercel it reads x-vercel-cache, and that unmapped values are reported as unknown with the raw value kept. Install it from the project README, then run:

sitewarm scan --from-sitemap https://example.com/sitemap.xml --top 10 --max-wait 0 --format json --report sitewarm.json
  • --from-sitemap reads the sitemap and follows sitemap indexes. URLs your robots.txt disallows for sitewarm are not requested and are reported as skipped-robots.
  • --top 10 keeps the first 10 allowed URLs. Raise it for a wider look.
  • --max-wait 0 makes one request per URL and profile. The default retries a miss or stale URL every 10 seconds for up to 5 minutes, which is what you want to warm a cache, not to read its current state.
  • --format json prints the report, and --report sitewarm.json also writes it to a file. Each result has cdn, cache_status_raw (the raw x-vercel-cache value), state, pop when exposed, cache_control, age and vary.

Because the default --fail-on list includes miss, the exit code is 1 when any result is a miss. That reports the finding. It is not an error in the tool. Requests are sent with a sitewarm/<version> token in the User-Agent, and sitewarm never impersonates a crawler.

A request can itself change what you see. A MISS that Vercel stores means the next request for that path in that region can be a HIT, so run the command twice and compare the raw values. This is the illustrative shape of one result, with invented values and a placeholder URL:

{
  "url": "https://example.com/blog/post",
  "profile": "desktop",
  "cdn": "vercel",
  "cache_status_raw": "<one of the six values>",
  "state": "<state from the README table>",
  "cache_control": "<what the browser was sent>"
}

The field names are the README's. The values in angle brackets are placeholders, not output.

One request from one place proves one location

The Vercel pages say the CDN cache is segmented by region. The README states the same limit from the other side: a hit is verified at the CDN PoP serving the machine that runs the scan, which is one location. A HIT from your laptop says nothing about a visitor served from another region, whose entry may be cold, evicted or not yet written. A MISS from CI says nothing about a visitor in a region that already has the entry.

Treat one run as a statement about one place at one moment. To see a different location, run the same command from a different network. Neither this article nor the sources read say how many regions there are or which one a given network reaches.

A short checklist

  • Read x-vercel-cache for the CDN and cache-control for the browser. They answer different questions.
  • Run the scan twice. If the first run shows MISS and the second HIT, the page is cacheable at that location.
  • If a page that should be cached stays MISS, check the criteria list: cookies set on the response, private or no-store, a Vary on Cookie.
  • After a new deployment, expect cold entries. Vercel scopes cached responses to the deployment that produced them.
  • If you see BYPASS on a route you did not mean to exclude, check Draft Mode and the other reasons on the cache status page.

Where sitewarm fits

sitewarm requests pages from your sitemap, up to the scan's URL limit, and reports the cache state the CDN returns for each one, with the vantage point it was measured from.

Frequently asked questions

What does x-vercel-cache tell me on a Next.js page?

It shows what happened to the cache lookup for that response. Vercel documents six values: HIT, MISS, STALE, PRERENDER, REVALIDATED and BYPASS. See how to read cache headers for the general method.

Why does a page with s-maxage show cache-control max-age=0?

Vercel's documentation says its proxy consumes s-maxage and returns cache-control: public, max-age=0, must-revalidate to the client. The header describes what the browser was told, not whether the CDN cached the page.

Does one HIT mean every visitor gets a cached page?

No. Vercel documents its CDN cache as segmented by region, and the README of the scan tool says a hit is verified at the CDN location serving the machine that runs the scan, which is one location. One request proves one location at one moment.

Is x-nextjs-cache the same as x-vercel-cache?

No. They are two headers. The Next.js ISR guide documents x-nextjs-cache with the values HIT, STALE, MISS and REVALIDATED. The Vercel pages document x-vercel-cache with six values. Check which headers your deployment actually returns.

References

  1. Vercel: CDN cachehttps://vercel.com/docs/caching/cdn-cache
  2. Vercel: Cache-Control headershttps://vercel.com/docs/caching/cache-control-headers
  3. Vercel: Response headershttps://vercel.com/docs/headers/response-headers
  4. Vercel: Cache status and reasonshttps://vercel.com/docs/caching/cache-status
  5. Vercel: Incremental Static Regenerationhttps://vercel.com/docs/incremental-static-regeneration
  6. Next.js: How to implement Incremental Static Regenerationhttps://nextjs.org/docs/app/guides/incremental-static-regeneration
  7. sitewarm: READMEhttps://github.com/sitewarm/sitewarm

See the cache state your CDN returns for each page

sitewarm requests pages from your sitemap, up to the scan's URL limit, after a deploy and reports the cache state the CDN returns for each one, with the vantage point it was measured from.

Start warming