Cache Status and Reasons
Vercel records how each cacheable request resolves as a cache status, and when the response wasn't a plain hit, a reason that explains it. The status tells you what happened to the cache lookup, and the reason tells you why. Read them together to see whether a response came from the cache, and if not, what sent the request to your function or origin instead.
These statuses appear in the x-vercel-cache response header, and, with the reason and extra context, in the Cache section of a request in runtime logs.
For requests that aren't cached, some data used by the origin may still be cached through Runtime Cache.
Every cacheable request resolves to one of these statuses:
| Status | What it means |
|---|---|
HIT | The response came from the cache. |
MISS | The response wasn't in the cache, so Vercel generated it from your function or origin and, if cacheable, stored it for next time. |
BYPASS | Vercel skipped the cache for this request and served fresh content. |
STALE | The cache served an existing response that was no longer fresh, then refreshed it in the background. |
PRERENDER | The response was served from static storage, such as a page prerendered at build time. |
REVALIDATED | The cached entry had been deleted, so Vercel regenerated the response from your origin in the foreground. |
The response was served directly from the cache, with no function invocation or origin fetch needed.


A MISS means Vercel checked the cache, found nothing it could serve, and generated the response from your function or origin. Vercel stores the result for ISR and Partial Prerendering pages, or when the response sets cache-control headers, so a later request for the same path can be a HIT.
A miss isn't always a problem. Dynamic routes are generated on every request, so a miss there is expected and shows no reason. A reason appears only when the miss is worth explaining, such as on a route meant to be served from the cache, and tells you what sent the request to your function or origin instead.


The content wasn't in the cache yet, so this request generated it. This happens on the first request to a dynamic route or a page not prerendered at build time, after a new deployment (Vercel scopes cached responses to the deployment that produced them), and after a rarely-requested entry is evicted from a region's cache. A cold miss is expected for dynamic responses, but for ISR and Partial Prerendering it often means a path wasn't prerendered ahead of time, since those paths are meant to be served as a PRERENDER or HIT.
The request that finds a cold entry generates and stores the response, so later requests for the same path in the same region are served as a HIT. To reduce cold misses, prerender frequently-visited ISR and PPR paths at build time, and give other cacheable responses a longer lifetime. See CDN Cache and Incremental Static Regeneration.
Several visitors requested the same uncached path at the same time. Instead of invoking your function once per request, Vercel sent the first request to your origin to generate the response. It holds the rest until that response is ready, then serves them all the same result. The held requests show as a miss because they waited on the in-flight generation rather than finding a stored response. This shields your origin from a cache stampede during traffic spikes. See request collapsing.
Vercel couldn't read a stored response from the cache, so the request fell through to your function or origin. This happens when a cache-storage read fails, and Vercel generates the response fresh rather than serving a stored copy.
Your origin sent a Vary header naming a request header whose value is close to unique per visitor, such as Cookie. Storing the response would create a separate cache entry for nearly every request, so Vercel generates and serves the response without caching it. The reason names the headers that caused it, for example vary_key_denied:cookie.
To make the route cacheable, check whether the response really changes with that header. If it doesn't, remove it from Vary. If it does, the response is per-visitor and belongs behind Cache-Control: private rather than the CDN cache. See high-cardinality headers.
A BYPASS means Vercel skipped the cache on purpose for this request and served fresh content from your function or origin. Unlike a miss, it doesn't consult the cache at all, so the reason describes what opted this request out.


The request had Draft Mode (formerly Preview Mode) enabled, so Vercel skipped the cached prerender and rendered fresh content at request time. Draft Mode is triggered when the request's __prerender_bypass cookie matches the route's bypassToken. Frameworks like Next.js and SvelteKit use it so team members can preview unpublished CMS content with full styling, without serving it to other visitors or waiting for the cache to refresh.
The request matched the route's experimentalBypassFor configuration, so Vercel skipped the cached prerender and invoked your function to render the page fresh. Frameworks can configure this to match any request based on their own requirements. Next.js, for example, uses experimentalBypassFor to opt specific requests out of the prerender based on their headers, cookies, query parameters, or User-Agent, most commonly to serve matching crawlers fully rendered content for SEO. On Partial Prerendering routes, a matching bot User-Agent resolves to this same reason.
A request from a known crawler hit a path that would normally serve a prerender fallback, the lightweight shell shown while a page is generated for the first time. Vercel matched the crawler against its built-in list of User-Agent patterns (such as Googlebot, Bingbot, facebookexternalhit, Twitterbot, and Slackbot) and, rather than return the fallback, waited for the fully rendered page so the crawler indexes complete content. Unlike Prerender Bypass, this list is maintained by Vercel rather than configured per route.
A STALE status means the cache served an existing response that's no longer fresh, then regenerated it in the background by re-invoking your function or origin. The visitor still gets a fast response from the cache while Vercel regenerates the response in the background. The reason explains why the entry became stale.


The entry passed its revalidation window, so Vercel served the existing copy and started regenerating it in the background. This is the standard stale-while-revalidate path, including the revalidation intervals that frameworks like Next.js set through ISR.
Invalidating a cache tag attached to this entry marked it stale on the next request. Vercel served the existing response and regenerated it in the background. You invalidate tags through invalidateByTag() or a framework function like revalidateTag().
Vercel tried to regenerate the entry, but the request failed, for example because your function errored or your origin was unreachable. Rather than surface that failure to the visitor, Vercel kept serving the last good response and retries the revalidation on a later request.
The response was served from static storage, such as a page prerendered at build time. Frameworks configure this through the Build Output API. In Next.js, statically generated pages return PRERENDER. See the Next.js getStaticPaths docs for when a route is prerendered.


The cached entry had been deleted, so Vercel regenerated the response from your origin in the foreground. Because there's no stale copy to fall back on, the request waits for that regeneration and pays the full generation latency, unlike STALE, where an expired entry is served immediately while Vercel refreshes the next one in the background. An entry is deleted on demand through dangerouslyDeleteByTag(), a framework function like revalidatePath() or revalidateTag() called without a lifetime, or a dashboard purge that deletes by tag.


When the deletion was triggered by a cache tag — through dangerouslyDeleteByTag(), revalidateTag() called without a lifetime, or a dashboard purge that deletes by tag — the REVALIDATED response carries this reason. It's the deletion counterpart to a STALE response's Tag-based invalidation: both start from a tag, but invalidation keeps serving the existing copy while it refreshes in the background, whereas deletion leaves nothing to serve, so the request blocks on the foreground regeneration.
Was this helpful?