Cdn Caching
Debug Vercel CDN caching — cache hit rate, stale content, revalidation behavior, ISR + PPR, per-request cache reasons (cacheReason) and PPR state (ppr_state), and costs.
- Skill ID
- vercel/vercel-plugin/cdn-caching
- Publisher
- vercel
- Repository
- vercel-plugin
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
vercel/vercel-plugin/cdn-cachingInstalls these files- SKILL.md
What this skill tells the agent
Vercel Caching
You are an expert in understanding Vercel's caching infrastructure, and how the CDN Cache, ISR, and PPR work.
Core Knowledge
- ISR (and PPR, a rendering strategy built on it) is a framework feature — Next.js, SvelteKit, Nuxt, and Astro all use it on Vercel, and the layers, metrics, and CLI here apply regardless. (For caching data _between your function and a backend_, that's the Runtime Cache — a separate layer; see References.)
- PPR (Partial Prerendering) — a rendering strategy, _not_ a cache layer: the static shell lives in the ISR cache while a function renders the dynamic holes per request and streams them into the same response. A route with holes still invokes the function on a shell hit; a holeless route is just ISR (a pure
prerenderHIT).
How caching works
Vercel caches at multiple layers between the visitor and your backend. A request reaches the nearest PoP, which routes to a Vercel region; the CDN then checks each layer in order and returns a cached response as soon as one is available, so your function runs only when nothing upstream has a valid copy.
Cache layers
- CDN cache — regional, ephemeral. On a hit the region returns the response with no function call. Reads/writes are free.
- ISR cache — durable, in a single Function region. On a CDN miss, Vercel reads here _before_ invoking your function (cache shielding), then replicates the result back to the CDN. Survives deploys for 31 days or until revalidated; reads/writes are billed in 8 KB units.
- Function invocation — runs only if neither cache has a valid copy. It may read the Runtime/data cache (a separate layer; see References) and your backend, then Vercel stores the response in the ISR cache.
- Image cache — optimized images, cached on the CDN after the first transform.
- Purges propagate globally in ~300 ms.
Request collapsing: when many requests hit the same uncached path at once, Vercel collapses them into one function invocation per region to protect the origin.
Key concepts
- Cache hit rate — share served from cache (
HIT/STALE/PRERENDER) versus origin (MISS/REVALIDATED). Measure it over _cacheable_ requests — excludeBYPASSand(not set)(redirects, errors, uncacheable methods), or they drag the ratio down for non-cache reasons. Low hit rate means more origin load and higher latency. - Revalidation — refreshing cached content. Time-based runs automatically after an interval; on-demand runs when you call an API. Both use stale-while-revalidate: visitors keep getting the cached version while the new one regenerates in the background.
- Invalidate vs. dangerously-delete — two ways to clear content, with very different blast on hit rate:
- _Invalidate_ (
invalidateByTag, Next.jsrevalidateTag/revalidatePath) = stale-while-revalidate. Keeps serving stale while refreshing in the background → response showsx-vercel-cache: STALE. - _Dangerously-delete_ (
dangerouslyDeleteByTag, Next.jsupdateTagor a revalidate with no lifetime) = hard removal. The next request blocks in the foreground to regenerate →x-vercel-cache: REVALIDATED. - Cache tags & blast radius — tags group cached entries so one call can clear many. A coarse tag attached to thousands of paths has a large _blast radius_: a single write drops them all and the hit rate collapses until they re-warm. Prefer granular tags (
product-${id}) plus a roll-up tag. - Cache status (
x-vercel-cacheresponse header) — the _outcome_:
| Value | Meaning |
|---|---|
HIT | Served from cache; no function ran |
MISS | Not cached; origin/function ran |
STALE | Served stale while revalidating in background (SWR / invalidate) |
PRERENDER | Served a prerendered ISR/PPR shell |
REVALIDATED | Foreground revalidation after a delete (or Pragma: no-cache) |
BYPASS | Caching skipped (no-store, private, cookies, etc.) |
- Cache reason (
cacheReason) — the finer _explanation_ of that outcome for a single request. Thecache_resultmetric lumps allMISSes (and allSTALEs) together; the reason is the only thing that tells them apart. Nine values, three per group:
cacheReason | Refines | Meaning |
|---|---|---|
cold | MISS | Cache empty for this key/variant (first request or evicted); the function ran |
collapsed | MISS | Concurrent requests to one uncached path collapsed into a single invocation |
error | MISS | An error prevented serving from cache |
draft_mode | → BYPASS | Next.js Draft Mode active — bypassed so editors see live content |
prerender_bypass | → BYPASS | Prerender-bypass cookie/token present |
crawler | → BYPASS | SEO-crawler UA — full response served so bots index real content |
stale_time | STALE | Time-based revalidate interval elapsed; regenerating in background (SWR) |
stale_tag | STALE | Tag invalidated (revalidateTag / invalidateByTag); regenerating |
stale_error | STALE | A revalidation attempt failed; serving the last-good copy (a bug signal) |
A raw MISS with reason draft_mode / prerender_bypass / crawler is displayed as `BYPASS` (all usually expected). The three stale_* reasons separate a healthy time refresh (stale_time) from a broad-tag blast (stale_tag) from a failing regen (stale_error). Read cacheReason from vercel logs or the dashboard Logs "Reason" row — the x-vercel-cache-reason header is internal-only and not visible via curl.
- PPR state (
ppr_state) — for a Partial Prerendering route, _how much_ of the response was prerendered versus computed per request. Only set onpartial_prerenderserves; blank for plainprerender/func/staticroutes and for cases the proxy can't classify (cold shell miss,BYPASS). Three states:
ppr_state | Meaning |
|---|---|
| Static | Fully prerendered — no postponed state, so the function is not invoked for the body |
| Partial | A static shell serves from cache + a postponed hole the function resumes per request |
| Dynamic | The whole body is postponed and rendered by the function per request |
A Partial serve that still invokes the function is _not_ a cache miss — the cached shell serves immediately while the function fills only the dynamic holes. Read ppr_state from vercel logs or the dashboard Logs panel, or aggregate with vercel metrics vercel.request.count --group-by ppr_state. Like cacheReason, the x-vercel-ppr-state header is internal-only and not visible via curl.
Investigating cache issues
Reach for the Vercel CLI. vercel metrics gives aggregate numbers (requires Observability Plus); vercel logs shows per-request behavior.
More from vercel
Use this skill in a project
Open any RiverX project, search the Skills panel for vercel/vercel-plugin/cdn-caching, and install it in one click.
