From 9c6ec492cff33e5a1c6d063cb93b668a91dee802 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Wed, 1 Jul 2026 19:29:27 -0700 Subject: [PATCH 1/9] [skills] Add cache-reason skill for per-request cache reason debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `cache-reason` skill that explains the per-request `cacheReason` field (cold, collapsed, error, draft_mode, prerender_bypass, crawler, stale_time, stale_tag, stale_error) — why a single request was a MISS, STALE, or BYPASS. Complements `cdn-caching`, which covers aggregate hit rate and ISR cost via `vercel metrics`. Written for the public audience: all observation paths route through the dashboard Logs "Reason" row and `vercel logs --json`. The internal-only `x-vercel-cache-reason` response header (gated behind is_timing_header_visible) is explicitly flagged as not user-visible, and the skill avoids the not-yet-published /docs/caching/cache-status anchors. - New: skills/cache-reason/SKILL.md (with promptSignals + retrieval block) - vercel.md: graph reference in the caching section - README.md: skills table row + count - cdn-caching: mutual cross-link - Regenerated skill-manifest.json, skill-catalog.md, build-from-skills.manifest.json Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 +- generated/build-from-skills.manifest.json | 2 +- generated/skill-catalog.md | 17 ++- generated/skill-manifest.json | 92 ++++++++++++- skills/cache-reason/SKILL.md | 149 ++++++++++++++++++++++ skills/cdn-caching/SKILL.md | 1 + vercel.md | 1 + 7 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 skills/cache-reason/SKILL.md diff --git a/README.md b/README.md index bef2f8f..0a31239 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ A text-form relational graph covering: - Common cross-product workflows - Migration awareness for sunset products -### Skills (30 skills) +### Skills (31 skills) | Skill | Covers | |-------|--------| @@ -51,6 +51,7 @@ A text-form relational graph covering: | `ai-sdk` | AI SDK v6 — text/object generation, streaming, tool calling, agents, MCP, providers, embeddings | | `auth` | Authentication integrations — Clerk, Descope, Auth0 setup for Next.js with Marketplace provisioning | | `bootstrap` | Project bootstrapping orchestrator — linking, env provisioning, db setup, first-run commands | +| `cache-reason` | Explain the per-request cache reason (cold, collapsed, stale_tag, draft_mode, crawler…) — why one request was MISS/STALE/BYPASS | | `cdn-caching` | Diagnose cache hit rate, stale content, revalidation behavior, and ISR read/write cost across CDN/ISR/PPR | | `chat-sdk` | Multi-platform chat bots — Slack, Telegram, Teams, Discord, Google Chat, GitHub, Linear | | `deployments-cicd` | Deployment and CI/CD — deploy, promote, rollback, --prebuilt, CI workflow files | diff --git a/generated/build-from-skills.manifest.json b/generated/build-from-skills.manifest.json index 0bcb319..fea959c 100644 --- a/generated/build-from-skills.manifest.json +++ b/generated/build-from-skills.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-01T19:14:58.838Z", + "generatedAt": "2026-07-02T02:23:03.926Z", "templates": [ { "template": "agents/ai-architect.md.tmpl", diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index aa57b93..8bc0d6b 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,8 +1,8 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-01T19:14:58.864Z -> Skills: 30 +> Generated: 2026-07-02T02:23:06.014Z +> Skills: 31 ## Table of Contents @@ -29,6 +29,7 @@ | `vercel-storage` | 7 | 15 | 48 | 8 | | `verification` | 7 | 0 | 8 | 0 | | `auth` | 6 | 14 | 12 | 0 | +| `cache-reason` | 6 | 0 | 1 | 0 | | `cdn-caching` | 6 | 0 | 1 | 0 | | `deployments-cicd` | 6 | 6 | 7 | 0 | | `next-cache-components` | 6 | 5 | 1 | 1 | @@ -509,6 +510,14 @@ - `middleware.js` (path) - `src/middleware.ts` (path) +#### `cache-reason` (priority 6) + +**Bash patterns:** +- `\bvercel\s+logs\b` + +**Matched examples:** +- `vercel logs` (bash) + #### `cdn-caching` (priority 6) **Bash patterns:** @@ -940,7 +949,7 @@ Shows which skills compete on shared bash commands. | `vercel dev` | `vercel-functions` (8), `verification` (7), `vercel-cli` (4) | | `vercel env pull` | `ai-gateway` (7), `env-vars` (7), `vercel-cli` (4) | | `vercel env add` | `env-vars` (7), `vercel-cli` (4) | -| `vercel logs` | `vercel-functions` (8), `vercel-cli` (4) | +| `vercel logs` | `vercel-functions` (8), `cache-reason` (6), `vercel-cli` (4) | | `vercel build` | `deployments-cicd` (6), `vercel-cli` (4) | | `next dev` | `verification` (7), `next-cache-components` (6), `nextjs` (5) | | `next build` | `next-cache-components` (6), `nextjs` (5) | @@ -962,7 +971,7 @@ Shows which skills compete on shared bash commands. **Priority 7:** `ai-gateway`, `env-vars`, `microfrontends`, `vercel-firewall`, `vercel-storage`, `verification` -**Priority 6:** `auth`, `cdn-caching`, `deployments-cicd`, `next-cache-components`, `next-forge`, `next-upgrade`, `routing-middleware`, `runtime-cache`, `shadcn` +**Priority 6:** `auth`, `cache-reason`, `cdn-caching`, `deployments-cicd`, `next-cache-components`, `next-forge`, `next-upgrade`, `routing-middleware`, `runtime-cache`, `shadcn` **Priority 5:** `nextjs`, `vercel-connect` diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index 2b51877..515ed54 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-01T19:14:58.808Z", + "generatedAt": "2026-07-02T02:23:05.290Z", "version": 2, "skills": { "ai-gateway": { @@ -849,6 +849,96 @@ "examples": [] } }, + "cache-reason": { + "priority": 6, + "summary": "", + "docs": [ + "https://vercel.com/docs/caching", + "https://vercel.com/docs/caching/cdn-cache/debug-cache-issues", + "https://vercel.com/docs/cli/logs" + ], + "pathPatterns": [], + "bashPatterns": [ + "\\bvercel\\s+logs\\b" + ], + "importPatterns": [], + "bodyPath": "skills/cache-reason/SKILL.md", + "pathRegexSources": [], + "bashRegexSources": [ + "\\bvercel\\s+logs\\b" + ], + "importRegexSources": [], + "chainTo": [ + { + "pattern": "revalidateTag|invalidateByTag|updateTag|dangerouslyDeleteByTag", + "targetSkill": "cdn-caching", + "message": "Tag invalidation detected — loading cdn-caching for blast-radius and ISR write-cost analysis." + } + ], + "promptSignals": { + "phrases": [ + "cache reason", + "x-vercel-cache-reason", + "why is it stale", + "why is my page stale", + "stale_tag", + "stale_time", + "stale_error", + "prerender bypass", + "request collapsed" + ], + "allOf": [ + [ + "why", + "bypass" + ], + [ + "why", + "stale" + ], + [ + "cache", + "reason" + ] + ], + "anyOf": [ + "draft mode", + "crawler", + "revalidation", + "cold cache" + ], + "noneOf": [], + "minScore": 6 + }, + "retrieval": { + "aliases": [ + "cache reason", + "x-vercel-cache-reason", + "stale reason", + "bypass reason", + "cache miss reason" + ], + "intents": [ + "why is my page stale", + "why is this request a bypass", + "why was this a cache miss", + "explain the cache status of a request", + "debug why a request was not cached" + ], + "entities": [ + "cacheReason", + "stale_tag", + "stale_time", + "stale_error", + "draft_mode", + "prerender_bypass", + "crawler", + "request collapsed", + "cold cache" + ], + "examples": [] + } + }, "cdn-caching": { "priority": 6, "summary": "", diff --git a/skills/cache-reason/SKILL.md b/skills/cache-reason/SKILL.md new file mode 100644 index 0000000..8ffcc05 --- /dev/null +++ b/skills/cache-reason/SKILL.md @@ -0,0 +1,149 @@ +--- +name: cache-reason +description: Explain the per-request Vercel cache reason (cold, collapsed, stale_tag, draft_mode, crawler…) — why a single request was a MISS, STALE, or BYPASS. +metadata: + priority: 6 + docs: + - 'https://vercel.com/docs/caching' + - 'https://vercel.com/docs/caching/cdn-cache/debug-cache-issues' + - 'https://vercel.com/docs/cli/logs' + bashPatterns: + - '\bvercel\s+logs\b' + promptSignals: + phrases: + - 'cache reason' + - 'x-vercel-cache-reason' + - 'why is it stale' + - 'why is my page stale' + - 'stale_tag' + - 'stale_time' + - 'stale_error' + - 'prerender bypass' + - 'request collapsed' + allOf: + - [why, bypass] + - [why, stale] + - [cache, reason] + anyOf: + - 'draft mode' + - 'crawler' + - 'revalidation' + - 'cold cache' + minScore: 6 +retrieval: + aliases: + - cache reason + - x-vercel-cache-reason + - stale reason + - bypass reason + - cache miss reason + intents: + - why is my page stale + - why is this request a bypass + - why was this a cache miss + - explain the cache status of a request + - debug why a request was not cached + entities: + - cacheReason + - stale_tag + - stale_time + - stale_error + - draft_mode + - prerender_bypass + - crawler + - request collapsed + - cold cache +chainTo: + - + pattern: 'revalidateTag|invalidateByTag|updateTag|dangerouslyDeleteByTag' + targetSkill: cdn-caching + message: 'Tag invalidation detected — loading cdn-caching for blast-radius and ISR write-cost analysis.' +--- + +# Vercel Cache Reason + +You are an expert in Vercel's per-request **cache reason** — the field that explains *why* a single request produced the cache status it did. + +## What cache reason is + +Every proxied request carries two related signals: + +- **Cache status** (`x-vercel-cache` / the `cache` field, and the `cache_result` metrics dimension) — the *outcome*: `HIT`, `MISS`, `STALE`, `PRERENDER`, `REVALIDATED`, `BYPASS`. +- **Cache reason** (`cacheReason`) — the *explanation* that refines that outcome for one request: `cold`, `collapsed`, `error`, `draft_mode`, `prerender_bypass`, `crawler`, `stale_time`, `stale_tag`, `stale_error`. + +Cache reason is **per-request**, not aggregate. The `cache_result` metrics bucket lumps every `STALE` together; the reason tells you whether that stale served because a timer elapsed, a tag was invalidated, or a revalidation errored — the distinction you actually need to fix stale-content and ISR-cost problems. For aggregate hit-rate and ISR read/write **cost** analysis, use the `cdn-caching` skill; this skill is for *why one request behaved the way it did*. + +## The nine reasons + +Grouped by the status each one refines. + +### MISS — nothing cacheable was served + +| `cacheReason` | Label | Meaning | Typical fix | +| --- | --- | --- | --- | +| `cold` | Cold | Cache empty for this key/variant — first request, or the entry was evicted. The function ran to generate it. | Expected on first hit. Persistent `cold` on low-traffic paths usually means **too many cache-key variants** (flag/experiment precomputation, dynamic params) that never stay warm — collapse the variant matrix. | +| `collapsed` | Request Collapsed | Many concurrent requests hit the same uncached path at once; Vercel collapsed them into **one** origin invocation per region. The waiters report `collapsed`. | This is origin protection working as intended. Only a concern if a single hot path is constantly cold (see `cold`). | +| `error` | Error | An error path (e.g. an internal cache-layer error) prevented serving from cache. | Investigate the function/origin logs for that request — pair with `vercel logs`. | + +### BYPASS — caching intentionally skipped + +A raw status of `MISS` is **displayed as `BYPASS`** when the reason is one of these three. All are usually expected, not misconfiguration: + +| `cacheReason` | Label | Meaning | Typical fix | +| --- | --- | --- | --- | +| `draft_mode` | Draft Mode | Next.js **Draft Mode** is active, so cache is bypassed and editors see live content. | Expected for preview/editing sessions. If unexpected in production, a Draft Mode cookie is leaking to real users. | +| `prerender_bypass` | Prerender Bypass | A prerender-bypass cookie/token is present, so prerendered content is bypassed. | Expected during on-demand preview. Check for a stale/leaked bypass cookie if seen broadly. | +| `crawler` | Crawler | An SEO-crawler user-agent was detected; the prerender fallback is skipped so the bot receives the **full** response (important on PPR routes). | Expected. Manage verified crawlers with the `vercel-firewall` skill. | + +### STALE — served last-good copy while revalidating (SWR) + +The status is `STALE` for all three; the reason is the only way to tell them apart: + +| `cacheReason` | Label | Meaning | Typical fix | +| --- | --- | --- | --- | +| `stale_time` | Time-based revalidation | The time-based `revalidate` interval elapsed; the stale copy served while a fresh one regenerates in the background. | If write cost is high, the interval is too short for how often content actually changes — prefer tag-based on-demand invalidation. | +| `stale_tag` | Tag-based invalidation | A cache tag was invalidated (`revalidateTag` / `invalidateByTag`); stale served while regenerating. | If **unrelated** routes go `stale_tag` together, a broad tag has a large blast radius — scope tags to specific IDs (`product-${id}`). | +| `stale_error` | Revalidation error | A revalidation attempt **failed**; Vercel keeps serving the last-good copy rather than an error. | This is a real bug signal — the regeneration is throwing. Read the function logs for that route. | + +> **Key rule:** `MISS` + `draft_mode` / `prerender_bypass` / `crawler` → shown as `BYPASS`. So a "BYPASS" in the dashboard is one of those three reasons; the "Debugging BYPASS traffic" section of `cdn-caching` covers the aggregate view. + +## How to see the cache reason + +Cache reason is available to **every** Vercel user through logs — you do **not** need any special access. + +**1. Dashboard (fastest for one request).** Observability → **Logs** → open a request → the **Reason** row (under the Cache event) shows the human-readable label and a "Learn more" link. + +**2. `vercel logs` (agent-friendly).** The CLI surfaces `cacheReason` per request. Use JSON so it can be parsed: + +```bash +# Cache reason for recent requests to a deployment +vercel logs --json | jq -r '{path: .requestPath, cache: .cache, reason: .cacheReason}' + +# Only the requests that carry a reason, tallied +vercel logs --json \ + | jq -r 'select(.cacheReason != null and .cacheReason != "") | .cacheReason' \ + | sort | uniq -c | sort -rn +``` + +> **Do not tell users to `curl` for it.** The `x-vercel-cache-reason` **response header** is an internal debug header — it is gated (only emitted to allowlisted teams/builds) and will **not** appear in a normal `curl -I`. Use the Logs panel or `vercel logs` instead. Cache reason is also **not** a `vercel metrics` dimension (metrics exposes `cache_result` = status only); reason is per-request, so it lives in logs. + +## From reason to action + +- Repeated `stale_tag` across many unrelated routes → an over-broad cache tag. Grep the invalidation call site (`revalidateTag(`, `invalidateByTag(`, `updateTag(`) and scope it; then quantify write cost with `cdn-caching` → "Analyzing ISR costs". +- Persistent `cold` on many distinct variants → cache-key explosion (feature-flag/experiment precomputation, unbounded dynamic params). Collapse the matrix or retire finished experiments. +- Any `stale_error` → revalidation is failing; the page is silently frozen on old content. Read the route's function logs (`vercel logs --json`) for the throwing regeneration. +- Unexpected `draft_mode` / `prerender_bypass` in production → a preview/bypass cookie is reaching real users; audit where it's set. + +## Related skills + +- `cdn-caching` — aggregate cache hit rate, stale content, and ISR read/write **cost** via `vercel metrics` (`cache_result`, `isr_operation.*`). Use it once cache reason tells you *which* behavior to quantify. +- `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, and `revalidate` tuning (where `stale_time` / `stale_tag` originate in a Next.js app). +- `runtime-cache` — per-region data cache between your function and a backend (a different layer from CDN/ISR). +- `vercel-firewall` — manage verified SEO crawlers and abusive bots behind `crawler` BYPASS traffic. + +## References + +- Caching overview: https://vercel.com/docs/caching +- Diagnosing and fixing cache issues (full runbook): https://vercel.com/docs/caching/cdn-cache/debug-cache-issues +- Incremental Static Regeneration: https://vercel.com/docs/incremental-static-regeneration +- vercel logs CLI: https://vercel.com/docs/cli/logs diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index 42640fd..e04409b 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -202,6 +202,7 @@ Use `--json` so the agent can parse cache status, path, and timing fields progra ## Related skills +- `cache-reason` — the per-request `cacheReason` field (why one request was MISS/STALE/BYPASS: `cold`, `collapsed`, `stale_time`, `stale_tag`, `stale_error`, `draft_mode`, `prerender_bypass`, `crawler`). Use it to disambiguate a single request; this skill for the aggregate picture. - `vercel-firewall` — manage verified SEO crawlers, block abusive bots, and rate-limit junk BYPASS traffic. - `runtime-cache` — caching data _between your function and a backend_ (per-region key-value / data cache). A different layer from the CDN/ISR caches; use it to cache an API response or query result inside a function. - `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, and `revalidate` tuning (one framework's ISR/PPR controls). diff --git a/vercel.md b/vercel.md index 076d13b..4ff54ba 100644 --- a/vercel.md +++ b/vercel.md @@ -702,6 +702,7 @@ Three distinct caching systems serve different purposes. They can be used indepe ⤳ skill: runtime-cache — Per-region key-value caching with tag-based invalidation ⤳ skill: nextjs — `'use cache'`, `revalidatePath`, `revalidateTag` ⤳ skill: cdn-caching — Diagnose cache hit rate, stale content, and ISR read/write cost +⤳ skill: cache-reason — Explain the per-request cache reason (why one request was MISS/STALE/BYPASS) --- From 5a320c6e560009d7c48019a77cbce258a3155131 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Thu, 2 Jul 2026 18:22:28 -0700 Subject: [PATCH 2/9] [skills] Merge cache-reason guidance into cdn-caching instead of a new skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold the per-request cacheReason content into the existing cdn-caching skill (from #108) rather than shipping a separate cache-reason skill: - cdn-caching: add the 9-value cacheReason table (cold, collapsed, error, draft_mode, prerender_bypass, crawler, stale_time, stale_tag, stale_error) refining the x-vercel-cache status, the MISS→BYPASS displayed-status rule, and how to read cacheReason via `vercel logs --json` / the Logs "Reason" row (with the internal-only x-vercel-cache-reason header caveat). - Absorb cache-reason prompt signals + add a retrieval block so questions like "why is my page stale" / "what does stale_tag mean" route here. - Remove skills/cache-reason/ and revert its README/vercel.md wiring (back to 30 skills). - Regenerate manifest, catalog, build-from-skills manifest. Verified: 899 tests pass; the real UserPromptSubmit hook injects cdn-caching for the three cache-reason prompts and correctly ignores unrelated prompts. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 5 +- generated/build-from-skills.manifest.json | 2 +- generated/skill-catalog.md | 17 +-- generated/skill-manifest.json | 124 +++++++----------- skills/cache-reason/SKILL.md | 149 ---------------------- skills/cdn-caching/SKILL.md | 71 ++++++++++- vercel.md | 3 +- 7 files changed, 119 insertions(+), 252 deletions(-) delete mode 100644 skills/cache-reason/SKILL.md diff --git a/README.md b/README.md index 0a31239..42ae25c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ A text-form relational graph covering: - Common cross-product workflows - Migration awareness for sunset products -### Skills (31 skills) +### Skills (30 skills) | Skill | Covers | |-------|--------| @@ -51,8 +51,7 @@ A text-form relational graph covering: | `ai-sdk` | AI SDK v6 — text/object generation, streaming, tool calling, agents, MCP, providers, embeddings | | `auth` | Authentication integrations — Clerk, Descope, Auth0 setup for Next.js with Marketplace provisioning | | `bootstrap` | Project bootstrapping orchestrator — linking, env provisioning, db setup, first-run commands | -| `cache-reason` | Explain the per-request cache reason (cold, collapsed, stale_tag, draft_mode, crawler…) — why one request was MISS/STALE/BYPASS | -| `cdn-caching` | Diagnose cache hit rate, stale content, revalidation behavior, and ISR read/write cost across CDN/ISR/PPR | +| `cdn-caching` | Diagnose cache hit rate, stale content, revalidation behavior, per-request cache reasons, and ISR read/write cost across CDN/ISR/PPR | | `chat-sdk` | Multi-platform chat bots — Slack, Telegram, Teams, Discord, Google Chat, GitHub, Linear | | `deployments-cicd` | Deployment and CI/CD — deploy, promote, rollback, --prebuilt, CI workflow files | | `env-vars` | Environment variable management — .env files, vercel env commands, OIDC tokens | diff --git a/generated/build-from-skills.manifest.json b/generated/build-from-skills.manifest.json index fea959c..cdc1d03 100644 --- a/generated/build-from-skills.manifest.json +++ b/generated/build-from-skills.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-02T02:23:03.926Z", + "generatedAt": "2026-07-03T01:21:43.662Z", "templates": [ { "template": "agents/ai-architect.md.tmpl", diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index 8bc0d6b..36df011 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,8 +1,8 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-02T02:23:06.014Z -> Skills: 31 +> Generated: 2026-07-03T01:21:45.816Z +> Skills: 30 ## Table of Contents @@ -29,7 +29,6 @@ | `vercel-storage` | 7 | 15 | 48 | 8 | | `verification` | 7 | 0 | 8 | 0 | | `auth` | 6 | 14 | 12 | 0 | -| `cache-reason` | 6 | 0 | 1 | 0 | | `cdn-caching` | 6 | 0 | 1 | 0 | | `deployments-cicd` | 6 | 6 | 7 | 0 | | `next-cache-components` | 6 | 5 | 1 | 1 | @@ -510,14 +509,6 @@ - `middleware.js` (path) - `src/middleware.ts` (path) -#### `cache-reason` (priority 6) - -**Bash patterns:** -- `\bvercel\s+logs\b` - -**Matched examples:** -- `vercel logs` (bash) - #### `cdn-caching` (priority 6) **Bash patterns:** @@ -949,7 +940,7 @@ Shows which skills compete on shared bash commands. | `vercel dev` | `vercel-functions` (8), `verification` (7), `vercel-cli` (4) | | `vercel env pull` | `ai-gateway` (7), `env-vars` (7), `vercel-cli` (4) | | `vercel env add` | `env-vars` (7), `vercel-cli` (4) | -| `vercel logs` | `vercel-functions` (8), `cache-reason` (6), `vercel-cli` (4) | +| `vercel logs` | `vercel-functions` (8), `vercel-cli` (4) | | `vercel build` | `deployments-cicd` (6), `vercel-cli` (4) | | `next dev` | `verification` (7), `next-cache-components` (6), `nextjs` (5) | | `next build` | `next-cache-components` (6), `nextjs` (5) | @@ -971,7 +962,7 @@ Shows which skills compete on shared bash commands. **Priority 7:** `ai-gateway`, `env-vars`, `microfrontends`, `vercel-firewall`, `vercel-storage`, `verification` -**Priority 6:** `auth`, `cache-reason`, `cdn-caching`, `deployments-cicd`, `next-cache-components`, `next-forge`, `next-upgrade`, `routing-middleware`, `runtime-cache`, `shadcn` +**Priority 6:** `auth`, `cdn-caching`, `deployments-cicd`, `next-cache-components`, `next-forge`, `next-upgrade`, `routing-middleware`, `runtime-cache`, `shadcn` **Priority 5:** `nextjs`, `vercel-connect` diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index 515ed54..6f662a4 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-02T02:23:05.290Z", + "generatedAt": "2026-07-03T01:21:45.033Z", "version": 2, "skills": { "ai-gateway": { @@ -849,38 +849,43 @@ "examples": [] } }, - "cache-reason": { + "cdn-caching": { "priority": 6, "summary": "", "docs": [ "https://vercel.com/docs/caching", - "https://vercel.com/docs/caching/cdn-cache/debug-cache-issues", - "https://vercel.com/docs/cli/logs" + "https://vercel.com/docs/caching/cdn-cache", + "https://vercel.com/docs/incremental-static-regeneration", + "https://vercel.com/docs/cli/metrics" ], "pathPatterns": [], "bashPatterns": [ - "\\bvercel\\s+logs\\b" + "\\bvercel\\s+cache\\s+(purge|invalidate|dangerously-delete)\\b" ], "importPatterns": [], - "bodyPath": "skills/cache-reason/SKILL.md", + "bodyPath": "skills/cdn-caching/SKILL.md", "pathRegexSources": [], "bashRegexSources": [ - "\\bvercel\\s+logs\\b" + "\\bvercel\\s+cache\\s+(purge|invalidate|dangerously-delete)\\b" ], "importRegexSources": [], "chainTo": [ { - "pattern": "revalidateTag|invalidateByTag|updateTag|dangerouslyDeleteByTag", - "targetSkill": "cdn-caching", - "message": "Tag invalidation detected — loading cdn-caching for blast-radius and ISR write-cost analysis." + "pattern": "use cache|cacheLife|cacheTag", + "targetSkill": "next-cache-components", + "message": "Next.js cache directives detected — loading Cache Components guidance for revalidate/tag tuning." } ], "promptSignals": { "phrases": [ + "cache hit rate", + "isr cost", + "isr read units", + "isr write units", + "stale content", + "x-vercel-cache", "cache reason", "x-vercel-cache-reason", - "why is it stale", - "why is my page stale", "stale_tag", "stale_time", "stale_error", @@ -889,22 +894,36 @@ ], "allOf": [ [ - "why", - "bypass" + "cache", + "debug" + ], + [ + "stale", + "cache" + ], + [ + "revalidation", + "count" ], [ "why", "stale" ], + [ + "why", + "bypass" + ], [ "cache", "reason" ] ], "anyOf": [ + "revalidate", + "prerender", + "invalidate", "draft mode", "crawler", - "revalidation", "cold cache" ], "noneOf": [], @@ -912,21 +931,24 @@ }, "retrieval": { "aliases": [ + "cache hit rate", "cache reason", "x-vercel-cache-reason", - "stale reason", - "bypass reason", - "cache miss reason" + "stale content", + "isr cost", + "bypass reason" ], "intents": [ + "debug cache hit rate", "why is my page stale", "why is this request a bypass", "why was this a cache miss", - "explain the cache status of a request", - "debug why a request was not cached" + "analyze isr read and write cost", + "explain the cache status of a request" ], "entities": [ "cacheReason", + "cache_result", "stale_tag", "stale_time", "stale_error", @@ -934,70 +956,12 @@ "prerender_bypass", "crawler", "request collapsed", - "cold cache" + "PRERENDER", + "BYPASS" ], "examples": [] } }, - "cdn-caching": { - "priority": 6, - "summary": "", - "docs": [ - "https://vercel.com/docs/caching", - "https://vercel.com/docs/caching/cdn-cache", - "https://vercel.com/docs/incremental-static-regeneration", - "https://vercel.com/docs/cli/metrics" - ], - "pathPatterns": [], - "bashPatterns": [ - "\\bvercel\\s+cache\\s+(purge|invalidate|dangerously-delete)\\b" - ], - "importPatterns": [], - "bodyPath": "skills/cdn-caching/SKILL.md", - "pathRegexSources": [], - "bashRegexSources": [ - "\\bvercel\\s+cache\\s+(purge|invalidate|dangerously-delete)\\b" - ], - "importRegexSources": [], - "chainTo": [ - { - "pattern": "use cache|cacheLife|cacheTag", - "targetSkill": "next-cache-components", - "message": "Next.js cache directives detected — loading Cache Components guidance for revalidate/tag tuning." - } - ], - "promptSignals": { - "phrases": [ - "cache hit rate", - "isr cost", - "isr read units", - "isr write units", - "stale content", - "x-vercel-cache" - ], - "allOf": [ - [ - "cache", - "debug" - ], - [ - "stale", - "cache" - ], - [ - "revalidation", - "count" - ] - ], - "anyOf": [ - "revalidate", - "prerender", - "invalidate" - ], - "noneOf": [], - "minScore": 6 - } - }, "chat-sdk": { "priority": 8, "summary": "", diff --git a/skills/cache-reason/SKILL.md b/skills/cache-reason/SKILL.md deleted file mode 100644 index 8ffcc05..0000000 --- a/skills/cache-reason/SKILL.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: cache-reason -description: Explain the per-request Vercel cache reason (cold, collapsed, stale_tag, draft_mode, crawler…) — why a single request was a MISS, STALE, or BYPASS. -metadata: - priority: 6 - docs: - - 'https://vercel.com/docs/caching' - - 'https://vercel.com/docs/caching/cdn-cache/debug-cache-issues' - - 'https://vercel.com/docs/cli/logs' - bashPatterns: - - '\bvercel\s+logs\b' - promptSignals: - phrases: - - 'cache reason' - - 'x-vercel-cache-reason' - - 'why is it stale' - - 'why is my page stale' - - 'stale_tag' - - 'stale_time' - - 'stale_error' - - 'prerender bypass' - - 'request collapsed' - allOf: - - [why, bypass] - - [why, stale] - - [cache, reason] - anyOf: - - 'draft mode' - - 'crawler' - - 'revalidation' - - 'cold cache' - minScore: 6 -retrieval: - aliases: - - cache reason - - x-vercel-cache-reason - - stale reason - - bypass reason - - cache miss reason - intents: - - why is my page stale - - why is this request a bypass - - why was this a cache miss - - explain the cache status of a request - - debug why a request was not cached - entities: - - cacheReason - - stale_tag - - stale_time - - stale_error - - draft_mode - - prerender_bypass - - crawler - - request collapsed - - cold cache -chainTo: - - - pattern: 'revalidateTag|invalidateByTag|updateTag|dangerouslyDeleteByTag' - targetSkill: cdn-caching - message: 'Tag invalidation detected — loading cdn-caching for blast-radius and ISR write-cost analysis.' ---- - -# Vercel Cache Reason - -You are an expert in Vercel's per-request **cache reason** — the field that explains *why* a single request produced the cache status it did. - -## What cache reason is - -Every proxied request carries two related signals: - -- **Cache status** (`x-vercel-cache` / the `cache` field, and the `cache_result` metrics dimension) — the *outcome*: `HIT`, `MISS`, `STALE`, `PRERENDER`, `REVALIDATED`, `BYPASS`. -- **Cache reason** (`cacheReason`) — the *explanation* that refines that outcome for one request: `cold`, `collapsed`, `error`, `draft_mode`, `prerender_bypass`, `crawler`, `stale_time`, `stale_tag`, `stale_error`. - -Cache reason is **per-request**, not aggregate. The `cache_result` metrics bucket lumps every `STALE` together; the reason tells you whether that stale served because a timer elapsed, a tag was invalidated, or a revalidation errored — the distinction you actually need to fix stale-content and ISR-cost problems. For aggregate hit-rate and ISR read/write **cost** analysis, use the `cdn-caching` skill; this skill is for *why one request behaved the way it did*. - -## The nine reasons - -Grouped by the status each one refines. - -### MISS — nothing cacheable was served - -| `cacheReason` | Label | Meaning | Typical fix | -| --- | --- | --- | --- | -| `cold` | Cold | Cache empty for this key/variant — first request, or the entry was evicted. The function ran to generate it. | Expected on first hit. Persistent `cold` on low-traffic paths usually means **too many cache-key variants** (flag/experiment precomputation, dynamic params) that never stay warm — collapse the variant matrix. | -| `collapsed` | Request Collapsed | Many concurrent requests hit the same uncached path at once; Vercel collapsed them into **one** origin invocation per region. The waiters report `collapsed`. | This is origin protection working as intended. Only a concern if a single hot path is constantly cold (see `cold`). | -| `error` | Error | An error path (e.g. an internal cache-layer error) prevented serving from cache. | Investigate the function/origin logs for that request — pair with `vercel logs`. | - -### BYPASS — caching intentionally skipped - -A raw status of `MISS` is **displayed as `BYPASS`** when the reason is one of these three. All are usually expected, not misconfiguration: - -| `cacheReason` | Label | Meaning | Typical fix | -| --- | --- | --- | --- | -| `draft_mode` | Draft Mode | Next.js **Draft Mode** is active, so cache is bypassed and editors see live content. | Expected for preview/editing sessions. If unexpected in production, a Draft Mode cookie is leaking to real users. | -| `prerender_bypass` | Prerender Bypass | A prerender-bypass cookie/token is present, so prerendered content is bypassed. | Expected during on-demand preview. Check for a stale/leaked bypass cookie if seen broadly. | -| `crawler` | Crawler | An SEO-crawler user-agent was detected; the prerender fallback is skipped so the bot receives the **full** response (important on PPR routes). | Expected. Manage verified crawlers with the `vercel-firewall` skill. | - -### STALE — served last-good copy while revalidating (SWR) - -The status is `STALE` for all three; the reason is the only way to tell them apart: - -| `cacheReason` | Label | Meaning | Typical fix | -| --- | --- | --- | --- | -| `stale_time` | Time-based revalidation | The time-based `revalidate` interval elapsed; the stale copy served while a fresh one regenerates in the background. | If write cost is high, the interval is too short for how often content actually changes — prefer tag-based on-demand invalidation. | -| `stale_tag` | Tag-based invalidation | A cache tag was invalidated (`revalidateTag` / `invalidateByTag`); stale served while regenerating. | If **unrelated** routes go `stale_tag` together, a broad tag has a large blast radius — scope tags to specific IDs (`product-${id}`). | -| `stale_error` | Revalidation error | A revalidation attempt **failed**; Vercel keeps serving the last-good copy rather than an error. | This is a real bug signal — the regeneration is throwing. Read the function logs for that route. | - -> **Key rule:** `MISS` + `draft_mode` / `prerender_bypass` / `crawler` → shown as `BYPASS`. So a "BYPASS" in the dashboard is one of those three reasons; the "Debugging BYPASS traffic" section of `cdn-caching` covers the aggregate view. - -## How to see the cache reason - -Cache reason is available to **every** Vercel user through logs — you do **not** need any special access. - -**1. Dashboard (fastest for one request).** Observability → **Logs** → open a request → the **Reason** row (under the Cache event) shows the human-readable label and a "Learn more" link. - -**2. `vercel logs` (agent-friendly).** The CLI surfaces `cacheReason` per request. Use JSON so it can be parsed: - -```bash -# Cache reason for recent requests to a deployment -vercel logs --json | jq -r '{path: .requestPath, cache: .cache, reason: .cacheReason}' - -# Only the requests that carry a reason, tallied -vercel logs --json \ - | jq -r 'select(.cacheReason != null and .cacheReason != "") | .cacheReason' \ - | sort | uniq -c | sort -rn -``` - -> **Do not tell users to `curl` for it.** The `x-vercel-cache-reason` **response header** is an internal debug header — it is gated (only emitted to allowlisted teams/builds) and will **not** appear in a normal `curl -I`. Use the Logs panel or `vercel logs` instead. Cache reason is also **not** a `vercel metrics` dimension (metrics exposes `cache_result` = status only); reason is per-request, so it lives in logs. - -## From reason to action - -- Repeated `stale_tag` across many unrelated routes → an over-broad cache tag. Grep the invalidation call site (`revalidateTag(`, `invalidateByTag(`, `updateTag(`) and scope it; then quantify write cost with `cdn-caching` → "Analyzing ISR costs". -- Persistent `cold` on many distinct variants → cache-key explosion (feature-flag/experiment precomputation, unbounded dynamic params). Collapse the matrix or retire finished experiments. -- Any `stale_error` → revalidation is failing; the page is silently frozen on old content. Read the route's function logs (`vercel logs --json`) for the throwing regeneration. -- Unexpected `draft_mode` / `prerender_bypass` in production → a preview/bypass cookie is reaching real users; audit where it's set. - -## Related skills - -- `cdn-caching` — aggregate cache hit rate, stale content, and ISR read/write **cost** via `vercel metrics` (`cache_result`, `isr_operation.*`). Use it once cache reason tells you *which* behavior to quantify. -- `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, and `revalidate` tuning (where `stale_time` / `stale_tag` originate in a Next.js app). -- `runtime-cache` — per-region data cache between your function and a backend (a different layer from CDN/ISR). -- `vercel-firewall` — manage verified SEO crawlers and abusive bots behind `crawler` BYPASS traffic. - -## References - -- Caching overview: https://vercel.com/docs/caching -- Diagnosing and fixing cache issues (full runbook): https://vercel.com/docs/caching/cdn-cache/debug-cache-issues -- Incremental Static Regeneration: https://vercel.com/docs/incremental-static-regeneration -- vercel logs CLI: https://vercel.com/docs/cli/logs diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index e04409b..0b180a9 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -1,6 +1,6 @@ --- name: cdn-caching -description: Debug Vercel CDN caching — cache hit rate, stale content, revalidation behavior, ISR + PPR, and costs. +description: Debug Vercel CDN caching — cache hit rate, stale content, revalidation behavior, ISR + PPR, per-request cache reasons, and costs. metadata: priority: 6 docs: @@ -18,15 +18,55 @@ metadata: - 'isr write units' - 'stale content' - 'x-vercel-cache' + - 'cache reason' + - 'x-vercel-cache-reason' + - 'stale_tag' + - 'stale_time' + - 'stale_error' + - 'prerender bypass' + - 'request collapsed' allOf: - [cache, debug] - [stale, cache] - [revalidation, count] + - [why, stale] + - [why, bypass] + - [cache, reason] anyOf: - 'revalidate' - 'prerender' - 'invalidate' + - 'draft mode' + - 'crawler' + - 'cold cache' minScore: 6 +retrieval: + aliases: + - cache hit rate + - cache reason + - x-vercel-cache-reason + - stale content + - isr cost + - bypass reason + intents: + - debug cache hit rate + - why is my page stale + - why is this request a bypass + - why was this a cache miss + - analyze isr read and write cost + - explain the cache status of a request + entities: + - cacheReason + - cache_result + - stale_tag + - stale_time + - stale_error + - draft_mode + - prerender_bypass + - crawler + - request collapsed + - PRERENDER + - BYPASS chainTo: - pattern: 'use cache|cacheLife|cacheTag' @@ -65,7 +105,7 @@ Vercel caches at multiple layers between the visitor and your backend. A request - _Invalidate_ (`invalidateByTag`, Next.js `revalidateTag`/`revalidatePath`) = stale-while-revalidate. Keeps serving stale while refreshing in the background → response shows `x-vercel-cache: STALE`. - _Dangerously-delete_ (`dangerouslyDeleteByTag`, Next.js `updateTag` or 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 / cache reason** (`x-vercel-cache` response header): +- **Cache status** (`x-vercel-cache` response header) — the _outcome_: | Value | Meaning | | ------------- | ---------------------------------------------------------------- | @@ -76,6 +116,22 @@ Vercel caches at multiple layers between the visitor and your backend. A request | `REVALIDATED` | Foreground revalidation after a delete (or `Pragma: no-cache`) | | `BYPASS` | Caching skipped (`no-store`, `private`, cookies, etc.) | +- **Cache reason** (`cacheReason`) — the finer _explanation_ that refines that outcome for a single request. Where the status says _what_ happened, the reason says _why_ — and it's the only way to tell three different `STALE`s (or three different `MISS`es) apart, since the aggregate `cache_result` metrics bucket lumps them together. Nine values, grouped by the status each refines: + + | `cacheReason` | Refines | Meaning | + | ------------------ | -------- | ------------------------------------------------------------------------------------ | + | `cold` | MISS | Cache empty for this key/variant (first request or evicted); the function ran | + | `collapsed` | MISS | Concurrent requests to the same uncached path collapsed into one origin invocation | + | `error` | MISS | An error path prevented serving from cache | + | `draft_mode` | → BYPASS | Next.js Draft Mode active — cache intentionally bypassed so editors see live content | + | `prerender_bypass` | → BYPASS | Prerender-bypass cookie/token present | + | `crawler` | → BYPASS | SEO-crawler UA — prerender fallback skipped so the bot gets the full response | + | `stale_time` | STALE | Time-based `revalidate` interval elapsed; regenerating in background (SWR) | + | `stale_tag` | STALE | A cache tag was invalidated (`revalidateTag` / `invalidateByTag`); regenerating | + | `stale_error` | STALE | A revalidation attempt **failed**; keeps serving the last-good copy (a real bug signal) | + + **Displayed-status rule:** a raw `MISS` with reason `draft_mode` / `prerender_bypass` / `crawler` is **shown as `BYPASS`** — so a "BYPASS" in logs is one of those three, all usually expected (see [Debugging BYPASS traffic](#debugging-bypass-traffic)). The three `stale_*` reasons are what let you distinguish a healthy time-based refresh (`stale_time`) from a broad-tag blast (`stale_tag`, → [Analyzing ISR costs](#analyzing-isr-costs)) from a failing regeneration (`stale_error`). + ## Investigating cache issues Reach for the Vercel CLI. `vercel metrics` gives aggregate numbers (requires [Observability Plus](https://vercel.com/docs/observability/observability-plus)); `vercel logs` shows per-request behavior. @@ -192,7 +248,15 @@ This zero-dependency first reach shows the status (`x-vercel-cache`), the cache vercel logs --request-id --json ``` -Use `--json` so the agent can parse cache status, path, and timing fields programmatically. +Use `--json` so the agent can parse cache status, path, and timing fields programmatically — including **`cacheReason`** (the per-request `cold` / `stale_tag` / `crawler` / … value from Key concepts), which is exposed on every log record and shown in the dashboard Logs "Reason" row. Group it to see why a bucket of traffic behaved a certain way: + +```bash +vercel logs --json \ + | jq -r 'select(.cacheReason != null and .cacheReason != "") | .cacheReason' \ + | sort | uniq -c | sort -rn +``` + +> Note: `cacheReason` lives in **logs**, not `vercel metrics` (which exposes `cache_result` = status only). The `x-vercel-cache-reason` **response header** is an internal debug header (gated; not visible in a normal `curl -I`) — use the Logs panel or `vercel logs` instead. ## FAQ @@ -202,7 +266,6 @@ Use `--json` so the agent can parse cache status, path, and timing fields progra ## Related skills -- `cache-reason` — the per-request `cacheReason` field (why one request was MISS/STALE/BYPASS: `cold`, `collapsed`, `stale_time`, `stale_tag`, `stale_error`, `draft_mode`, `prerender_bypass`, `crawler`). Use it to disambiguate a single request; this skill for the aggregate picture. - `vercel-firewall` — manage verified SEO crawlers, block abusive bots, and rate-limit junk BYPASS traffic. - `runtime-cache` — caching data _between your function and a backend_ (per-region key-value / data cache). A different layer from the CDN/ISR caches; use it to cache an API response or query result inside a function. - `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, and `revalidate` tuning (one framework's ISR/PPR controls). diff --git a/vercel.md b/vercel.md index 4ff54ba..e760184 100644 --- a/vercel.md +++ b/vercel.md @@ -701,8 +701,7 @@ Three distinct caching systems serve different purposes. They can be used indepe ⤳ skill: runtime-cache — Per-region key-value caching with tag-based invalidation ⤳ skill: nextjs — `'use cache'`, `revalidatePath`, `revalidateTag` -⤳ skill: cdn-caching — Diagnose cache hit rate, stale content, and ISR read/write cost -⤳ skill: cache-reason — Explain the per-request cache reason (why one request was MISS/STALE/BYPASS) +⤳ skill: cdn-caching — Diagnose cache hit rate, stale content, per-request cache reasons, and ISR read/write cost --- From d43d49a6e565f962f19ac873ad8efc81fa3278c3 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Mon, 6 Jul 2026 13:40:04 -0600 Subject: [PATCH 3/9] =?UTF-8?q?[skills]=20Tighten=20cdn-caching=20cacheRea?= =?UTF-8?q?son=20merge=20(287=E2=86=92133=20lines)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Condense the skill while keeping all 6 cache statuses and all 9 cache reasons (3 MISS: cold/collapsed/error, 3 BYPASS: draft_mode/ prerender_bypass/crawler, 3 STALE: stale_time/stale_tag/stale_error). Cut verbose prose, repeated metrics examples, and the FAQ; keep the two reference tables, the core metrics/logs investigation commands, and the cacheReason observation guidance. Add [cache, miss] prompt signal so MISS questions trigger. Verified: 899 tests pass; the real UserPromptSubmit hook injects cdn-caching for MISS/STALE/BYPASS/hit-rate prompts and ignores unrelated and generic-error prompts. Co-Authored-By: Claude Opus 4.8 (1M context) --- generated/build-from-skills.manifest.json | 2 +- generated/skill-catalog.md | 2 +- generated/skill-manifest.json | 42 +--- skills/cdn-caching/SKILL.md | 254 +++++----------------- 4 files changed, 67 insertions(+), 233 deletions(-) diff --git a/generated/build-from-skills.manifest.json b/generated/build-from-skills.manifest.json index cdc1d03..b56fbbe 100644 --- a/generated/build-from-skills.manifest.json +++ b/generated/build-from-skills.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-03T01:21:43.662Z", + "generatedAt": "2026-07-06T19:39:32.893Z", "templates": [ { "template": "agents/ai-architect.md.tmpl", diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index 36df011..78deea1 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,7 +1,7 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-03T01:21:45.816Z +> Generated: 2026-07-06T19:39:35.006Z > Skills: 30 ## Table of Contents diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index 6f662a4..7fcee90 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-03T01:21:45.033Z", + "generatedAt": "2026-07-06T19:39:34.292Z", "version": 2, "skills": { "ai-gateway": { @@ -854,9 +854,9 @@ "summary": "", "docs": [ "https://vercel.com/docs/caching", - "https://vercel.com/docs/caching/cdn-cache", "https://vercel.com/docs/incremental-static-regeneration", - "https://vercel.com/docs/cli/metrics" + "https://vercel.com/docs/cli/metrics", + "https://vercel.com/docs/cli/logs" ], "pathPatterns": [], "bashPatterns": [ @@ -873,23 +873,17 @@ { "pattern": "use cache|cacheLife|cacheTag", "targetSkill": "next-cache-components", - "message": "Next.js cache directives detected — loading Cache Components guidance for revalidate/tag tuning." + "message": "Next.js cache directives detected — loading Cache Components guidance." } ], "promptSignals": { "phrases": [ "cache hit rate", "isr cost", - "isr read units", - "isr write units", "stale content", "x-vercel-cache", "cache reason", - "x-vercel-cache-reason", "stale_tag", - "stale_time", - "stale_error", - "prerender bypass", "request collapsed" ], "allOf": [ @@ -901,10 +895,6 @@ "stale", "cache" ], - [ - "revalidation", - "count" - ], [ "why", "stale" @@ -913,6 +903,10 @@ "why", "bypass" ], + [ + "cache", + "miss" + ], [ "cache", "reason" @@ -920,9 +914,7 @@ ], "anyOf": [ "revalidate", - "prerender", "invalidate", - "draft mode", "crawler", "cold cache" ], @@ -931,33 +923,23 @@ }, "retrieval": { "aliases": [ - "cache hit rate", "cache reason", - "x-vercel-cache-reason", - "stale content", - "isr cost", - "bypass reason" + "cache hit rate", + "stale content" ], "intents": [ - "debug cache hit rate", "why is my page stale", "why is this request a bypass", - "why was this a cache miss", - "analyze isr read and write cost", - "explain the cache status of a request" + "why was this a cache miss" ], "entities": [ "cacheReason", - "cache_result", "stale_tag", - "stale_time", "stale_error", "draft_mode", "prerender_bypass", "crawler", - "request collapsed", - "PRERENDER", - "BYPASS" + "cold" ], "examples": [] } diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index 0b180a9..aa0a945 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -1,281 +1,133 @@ --- name: cdn-caching -description: Debug Vercel CDN caching — cache hit rate, stale content, revalidation behavior, ISR + PPR, per-request cache reasons, and costs. +description: Debug Vercel CDN caching — cache hit rate, stale content, revalidation, ISR + PPR, per-request cache reasons (cacheReason), and ISR cost. metadata: priority: 6 docs: - 'https://vercel.com/docs/caching' - - 'https://vercel.com/docs/caching/cdn-cache' - 'https://vercel.com/docs/incremental-static-regeneration' - 'https://vercel.com/docs/cli/metrics' + - 'https://vercel.com/docs/cli/logs' bashPatterns: - '\bvercel\s+cache\s+(purge|invalidate|dangerously-delete)\b' promptSignals: phrases: - 'cache hit rate' - 'isr cost' - - 'isr read units' - - 'isr write units' - 'stale content' - 'x-vercel-cache' - 'cache reason' - - 'x-vercel-cache-reason' - 'stale_tag' - - 'stale_time' - - 'stale_error' - - 'prerender bypass' - 'request collapsed' allOf: - [cache, debug] - [stale, cache] - - [revalidation, count] - [why, stale] - [why, bypass] + - [cache, miss] - [cache, reason] anyOf: - 'revalidate' - - 'prerender' - 'invalidate' - - 'draft mode' - 'crawler' - 'cold cache' minScore: 6 retrieval: aliases: - - cache hit rate - cache reason - - x-vercel-cache-reason + - cache hit rate - stale content - - isr cost - - bypass reason intents: - - debug cache hit rate - why is my page stale - why is this request a bypass - why was this a cache miss - - analyze isr read and write cost - - explain the cache status of a request entities: - cacheReason - - cache_result - stale_tag - - stale_time - stale_error - draft_mode - prerender_bypass - crawler - - request collapsed - - PRERENDER - - BYPASS + - cold chainTo: - pattern: 'use cache|cacheLife|cacheTag' targetSkill: next-cache-components - message: 'Next.js cache directives detected — loading Cache Components guidance for revalidate/tag tuning.' + message: 'Next.js cache directives detected — loading Cache Components guidance.' --- # 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 `prerender` HIT). - -### 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](https://vercel.com/docs/functions/configuring-functions/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. +Expert guidance on Vercel's CDN Cache, ISR, and PPR: hit rate, stale content, revalidation, per-request cache reasons, and ISR cost. ISR/PPR are framework features (Next.js, SvelteKit, Nuxt, Astro) — the layers, metrics, and CLI below apply to all. -**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. +## How caching works -#### Key concepts +A request reaches the nearest PoP → a Vercel region; the CDN checks each layer in order and returns the first cached copy, so your function runs only on a full miss. -- **Cache hit rate** — share served from cache (`HIT`/`STALE`/`PRERENDER`) versus origin (`MISS`/`REVALIDATED`). Measure it over _cacheable_ requests — exclude `BYPASS` and `(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.js `revalidateTag`/`revalidatePath`) = stale-while-revalidate. Keeps serving stale while refreshing in the background → response shows `x-vercel-cache: STALE`. - - _Dangerously-delete_ (`dangerouslyDeleteByTag`, Next.js `updateTag` or 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-cache` response header) — the _outcome_: +- **CDN cache** — regional, ephemeral, free reads/writes. A HIT returns with no function call. +- **ISR cache** — durable, single region. Read on a CDN miss before invoking your function (shielding); billed in 8 KB units; survives deploys 31 days or until revalidated. +- **Function** — runs only if neither cache has a valid copy; its result is stored in ISR. +- **Request collapsing** — concurrent requests to one uncached path collapse into a single origin invocation per region. +- **PPR** — the static shell lives in the ISR cache; the function fills dynamic holes per request. A holeless route is plain ISR (a `prerender` HIT). - | 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 status vs. cache reason -- **Cache reason** (`cacheReason`) — the finer _explanation_ that refines that outcome for a single request. Where the status says _what_ happened, the reason says _why_ — and it's the only way to tell three different `STALE`s (or three different `MISS`es) apart, since the aggregate `cache_result` metrics bucket lumps them together. Nine values, grouped by the status each refines: +**Status** (`x-vercel-cache`) is the _outcome_; **reason** (`cacheReason`, in logs) is _why_. The reason is the only way to tell three `STALE`s — or three `MISS`es — apart, since the `cache_result` metric lumps them together. - | `cacheReason` | Refines | Meaning | - | ------------------ | -------- | ------------------------------------------------------------------------------------ | - | `cold` | MISS | Cache empty for this key/variant (first request or evicted); the function ran | - | `collapsed` | MISS | Concurrent requests to the same uncached path collapsed into one origin invocation | - | `error` | MISS | An error path prevented serving from cache | - | `draft_mode` | → BYPASS | Next.js Draft Mode active — cache intentionally bypassed so editors see live content | - | `prerender_bypass` | → BYPASS | Prerender-bypass cookie/token present | - | `crawler` | → BYPASS | SEO-crawler UA — prerender fallback skipped so the bot gets the full response | - | `stale_time` | STALE | Time-based `revalidate` interval elapsed; regenerating in background (SWR) | - | `stale_tag` | STALE | A cache tag was invalidated (`revalidateTag` / `invalidateByTag`); regenerating | - | `stale_error` | STALE | A revalidation attempt **failed**; keeps serving the last-good copy (a real bug signal) | +| Status | Meaning | +| --- | --- | +| `HIT` | Served from cache; no function ran | +| `MISS` | Not cached; origin/function ran | +| `STALE` | Served stale while revalidating in background (SWR) | +| `PRERENDER` | Served a prerendered ISR/PPR shell | +| `REVALIDATED` | Foreground regen after a delete (or `Pragma: no-cache`) | +| `BYPASS` | Caching skipped (`no-store`, `private`, cookies, etc.) | - **Displayed-status rule:** a raw `MISS` with reason `draft_mode` / `prerender_bypass` / `crawler` is **shown as `BYPASS`** — so a "BYPASS" in logs is one of those three, all usually expected (see [Debugging BYPASS traffic](#debugging-bypass-traffic)). The three `stale_*` reasons are what let you distinguish a healthy time-based refresh (`stale_time`) from a broad-tag blast (`stale_tag`, → [Analyzing ISR costs](#analyzing-isr-costs)) from a failing regeneration (`stale_error`). +| `cacheReason` | Refines | Meaning | +| --- | --- | --- | +| `cold` | MISS | Cache empty for this key/variant (first request or evicted) | +| `collapsed` | MISS | Concurrent requests collapsed into one origin 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 (SWR) | +| `stale_tag` | STALE | Tag invalidated (`revalidateTag`/`invalidateByTag`); regenerating | +| `stale_error` | STALE | Revalidation **failed**; serving last-good copy (a bug signal) | -## Investigating cache issues +A raw `MISS` with `draft_mode` / `prerender_bypass` / `crawler` is **displayed as `BYPASS`** (all usually expected). The `stale_*` reasons separate a healthy time refresh (`stale_time`) from a broad-tag blast (`stale_tag`) from a failing regen (`stale_error`). -Reach for the Vercel CLI. `vercel metrics` gives aggregate numbers (requires [Observability Plus](https://vercel.com/docs/observability/observability-plus)); `vercel logs` shows per-request behavior. +## Investigating -Metrics need to be queried by team and project (`-S -p `). Filter production with `-f "environment eq 'production'"` (there is no `--prod` flag). Run `vercel metrics schema ` to discover dimensions; use `-F json` for machine-readable output. With `-g`, remember **`--limit` is per time bucket** — omit `-g` when you need totals across the whole window. +`vercel metrics` gives aggregates (needs Observability Plus); `vercel logs` shows per-request behavior. Query metrics by `-S -p `, filter prod with `-f "environment eq 'production'"`, add `-F json` for machine output. -### Cache hit rate - -Start here for an overall picture of how well caching is working. - -**Step 1 — overall split.** Group `vercel.request.count` by `cache_result`. Treat `HIT`, `STALE`, and `PRERENDER` as cache-served; focus investigation on `MISS`. Exclude `BYPASS` and `(not set)` when computing a hit rate over _cacheable_ traffic (see [Debugging BYPASS traffic](#debugging-bypass-traffic)). `STALE` means stale-while-revalidate is working — dig into revalidation frequency in [Analyzing ISR costs](#analyzing-isr-costs), not here. +- **Hit rate** — group `vercel.request.count` by `cache_result` (HIT/STALE/PRERENDER = served; focus `MISS`; exclude `BYPASS`). Split `MISS` by `path_type`, then `request_path`. +- **ISR cost** — focus `vercel.isr_operation.write_units` (charged on every regen); the CDN shields ISR so `read_units` run far below request count. Group write_units by `cache_tags` — unrelated routes with near-identical counts mean a shared broad tag firing in lockstep. Confirm by grepping the `revalidateTag(` / `invalidateByTag(` / `updateTag(` call site. +- **BYPASS** — mostly Draft Mode + crawlers (expected); group by `bot_category` / `user_agent` to see what's left. Manage bots with the `vercel-firewall` skill. ```bash -vercel metrics vercel.request.count -S -p \ - -f "environment eq 'production'" --group-by cache_result --since 24h +vercel metrics vercel.request.count -S -p --group-by cache_result --since 24h +vercel metrics vercel.isr_operation.write_units -S -p -a sum --group-by cache_tags --since 24h ``` -**Step 2 — where misses concentrate.** Split the `MISS` bucket (and optionally `STALE`) by `path_type`, then by `route` or `request_path`: +**One request** — `curl -sSI ` shows `x-vercel-cache`, `x-matched-path` (reveals experiment precompute), `vary`, and `set-cookie` (forces BYPASS). For the reason, read logs — the `x-vercel-cache-reason` header is internal-only and not visible via curl: ```bash -vercel metrics vercel.request.count -S -p \ - -f "environment eq 'production' and cache_result eq 'MISS'" \ - --group-by path_type --since 24h - -vercel metrics vercel.request.count -S -p \ - -f "environment eq 'production' and cache_result eq 'MISS' and path_type eq 'prerender'" \ - --group-by request_path --since 24h +vercel logs --json | jq -r 'select(.cacheReason!="") | .cacheReason' | sort | uniq -c | sort -rn ``` -**What to expect:** `prerender` routes (static shells, ISR pages) should show a high share of `HIT`/`PRERENDER`. A `prerender` path with a disproportionate `MISS` count is your short list for per-path header inspection (`curl` above) and code review. - -`streaming_func` routes render dynamically by default, but you can still cache them with `Cache-Control` headers — matching requests are cached on the CDN. Each cache entry varies by `Vary` headers (cookies, RSC, etc.) as well as path and query parameters, so expect more cache keys and a lower hit rate than a fully static `prerender` route. - -### Analyzing ISR costs - -Once you know hit rate, quantify ISR spend and whether revalidation — not traffic volume — is driving it. - -**Utilization vs. ISR billing.** **Utilization** is `vercel.request.count` — total request volume. **ISR cost** is billed separately in 8 KB units: `read_units` when the regional CDN misses and falls through to the ISR cache, and `write_units` on every revalidation/regeneration. The regional CDN shields ISR heavily — most requests never touch the ISR layer, so **read_units will be far below request count**. Do not compare read_units to write_units as a utilization check; focus on **write_units** (revalidation cost) and how they relate to total traffic. - -```bash -vercel metrics vercel.request.count -S -p -a sum --since 24h -vercel metrics vercel.isr_operation.write_units -S -p -a sum --since 24h -``` - -**Which routes revalidate most.** Break write units down by `route` and `request_path` to find paths that regenerate often relative to traffic: - -```bash -vercel metrics vercel.isr_operation.write_units -S -p \ - -a sum --group-by route --since 24h - -vercel metrics vercel.isr_operation.write_units -S -p \ - -a sum --group-by request_path --since 24h -``` - -**Regeneration vs. serving.** Group write units by `path_type` — concentration in `background_func` confirms revalidation (not per-request dynamic work) is the cost driver. - -**Time-based vs. tag-based revalidation.** Time-based intervals regenerate on a schedule whether or not content changed — often inefficient. Tag-based on-demand revalidation is usually better, but an **overly broad tag** has a large blast radius: one invalidate drops every entry that carries it. - -- **Tag blast radius** — group write units by `cache_tags`. If many _unrelated_ routes show near-identical write counts, a shared hot tag is invalidating them in lockstep (e.g. every blog post rewriting at the same rate because they share one broad `blogPost` tag): - -```bash -vercel metrics vercel.isr_operation.write_units -S -p \ - -a sum --group-by cache_tags --since 24h -``` - -- **What triggered revalidation** — group `vercel.request.count` by `triggering_tag` to see which tags fire most often (`triggering_tag` is on request count only, not ISR operation metrics. It is one of the tags that triggered the page to be stale): - -```bash -vercel metrics vercel.request.count -S -p \ - -f "triggering_tag ne null" --group-by triggering_tag --since 24h -``` - -Tags with a large blast radius that revalidate frequently are the usual root cause of high write_units. Prefer granular tags (`product-${id}`) and on-demand invalidation over short time-based intervals for event-driven content. - -**Confirm in code.** Metrics tell you _which_ tag is hot; the repo tells you _why_. Grep for the tag's invalidation call site — `revalidateTag(`, `invalidateByTag(`, `updateTag(`, `dangerouslyDeleteByTag(` — and read the trigger. A CMS webhook or a sync cron that invalidates a **broad** tag on every event (instead of a specific `${type}:${id}`) is the classic amplifier. - -### Debugging BYPASS traffic - -The largest legitimate sources of `BYPASS` are **Draft Mode** and **SEO crawlers**. Draft Mode must bypass cache so editors see live content. SEO bots must receive the **full response** — especially on PPR routes where the static shell and dynamic holes are assembled at request time — so crawlers index what users actually see. That BYPASS is expected, not a misconfiguration. - -Before tuning headers or revalidate intervals, confirm what's left after those two buckets: - -```bash -vercel metrics vercel.request.count -S -p \ - -f "cache_result eq 'BYPASS'" --group-by bot_category --since 24h - -vercel metrics vercel.request.count -S -p \ - -f "cache_result eq 'BYPASS'" --group-by user_agent --since 24h - -vercel metrics vercel.request.count -S -p \ - -f "cache_result eq 'BYPASS'" --group-by request_method --since 24h -``` - -The **Firewall/WAF** with the `vercel-firewall` skill can be used to manage verified SEO crawlers, block abusive bots, and rate-limit junk traffic before it distorts your hit-rate picture. - ## Reducing ISR cost -- **Prefer tag-based over time-based revalidation.** Replace short `revalidate` intervals with on-demand `revalidateTag` / `invalidateByTag` when content changes — time-based regeneration runs whether or not anything changed. If using Cache Components, analyze `cacheLife` calls with the `next-cache-components` skill. -- **Scope tags to specific IDs.** Invalidate `blogPost:`, not a generic `blogPost`/`page` tag — one broad invalidate regenerates everything that carries it. -- Tune the revalidate interval where your framework declares it (Next.js `revalidate` / `cacheLife`, SvelteKit `isr`, Nuxt `routeRules`, Astro). For Next.js Cache Components, see the `next-cache-components` skill. -- Use `CDN-Cache-Control` headers to cache dynamic functions. - -### Inspect one path - -```bash -curl -sSI https:/// | grep -iE 'x-vercel-cache|x-matched-path|cache-control|vary|age|set-cookie' -``` - -This zero-dependency first reach shows the status (`x-vercel-cache`), the cache directives (`Cache-Control` / `CDN-Cache-Control` / `Vercel-CDN-Cache-Control`), and — crucially — **`x-matched-path`**, which reveals rewrites like `/precomputed/exp~.../...` that expose experiment/flag precomputation. `vary` flags personalization (RSC, cookies); `set-cookie` forces `BYPASS`. For a per-phase timing breakdown, `vercel httpstat /some/path` (CLI v48.9.0+; needs the `httpstat` tool installed) adds latency stats. A path that should cache but shows `MISS`/`BYPASS` usually has `private`, `no-store`, `max-age=0`, a per-request input (cookies/headers/`searchParams`), or an uncacheable method (see FAQ). - -**Inspect one request.** When metrics or headers give you a request ID, pull the full log record: - -```bash -vercel logs --request-id --json -``` - -Use `--json` so the agent can parse cache status, path, and timing fields programmatically — including **`cacheReason`** (the per-request `cold` / `stale_tag` / `crawler` / … value from Key concepts), which is exposed on every log record and shown in the dashboard Logs "Reason" row. Group it to see why a bucket of traffic behaved a certain way: - -```bash -vercel logs --json \ - | jq -r 'select(.cacheReason != null and .cacheReason != "") | .cacheReason' \ - | sort | uniq -c | sort -rn -``` - -> Note: `cacheReason` lives in **logs**, not `vercel metrics` (which exposes `cache_result` = status only). The `x-vercel-cache-reason` **response header** is an internal debug header (gated; not visible in a normal `curl -I`) — use the Logs panel or `vercel logs` instead. - -## FAQ - -- **What are prerender variant misses?** When a route uses a dynamic param, each distinct cache-key variant is prerendered and cached separately, so each variant misses on its first hit per region and low-traffic ones rarely stay warm. The most common modern cause is **feature-flag / experiment precomputation** — middleware picks a variant per request (`/precomputed/exp~.../...` paths), and flags × routes × PPR segments multiply into thousands of ISR entries (also a middleware-invocation cost). Fix: collapse the variant matrix (retire finished experiments), or accept the cost. -- **Does PPR avoid function invocations?** No — a PPR route has dynamic holes by definition, so the cached shell hit still runs the function to fill them. (A route with _no_ holes is just ISR and serves a pure `prerender` HIT — see Key concepts.) -- **Why are there more function invocations than PPR requests?** PPR requests have a static shell and a dynamic function invocation. When the static shell needs to be regenerated, it incurs a function invocation on top of the dynamic function for the content. +- Prefer tag-based on-demand revalidation over short time intervals (which regen whether content changed or not). +- Scope tags to IDs (`product-${id}`), not broad `page` / `blogPost` tags with a large blast radius. +- Tune the revalidate interval where the framework declares it; for Next.js `use cache` / `cacheLife`, see `next-cache-components`. ## Related skills -- `vercel-firewall` — manage verified SEO crawlers, block abusive bots, and rate-limit junk BYPASS traffic. -- `runtime-cache` — caching data _between your function and a backend_ (per-region key-value / data cache). A different layer from the CDN/ISR caches; use it to cache an API response or query result inside a function. -- `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, and `revalidate` tuning (one framework's ISR/PPR controls). +- `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, `revalidate` tuning. +- `runtime-cache` — per-region key-value cache between a function and a backend. +- `vercel-firewall` — verified crawlers, bot blocking, rate limits. -## References: +## References -- Caching overview: https://vercel.com/docs/caching -- ISR: https://vercel.com/docs/incremental-static-regeneration -- Partial Prerendering (PPR): https://vercel.com/docs/partial-prerendering -- Cache-Control headers: https://vercel.com/docs/caching/cache-control-headers -- Diagnosing and fixing cache issues (full runbook): https://vercel.com/docs/caching/cdn-cache/debug-cache-issues -- vercel metrics CLI: https://vercel.com/docs/cli/metrics -- vercel logs CLI: https://vercel.com/docs/cli/logs +- https://vercel.com/docs/caching · /incremental-static-regeneration · /partial-prerendering · /cli/metrics · /cli/logs From eaed2de12908e99027713a5565bd0e2f30af091b Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Mon, 6 Jul 2026 14:19:57 -0600 Subject: [PATCH 4/9] [skills] Complete cacheReason retrieval entities (add error, collapsed, stale_time) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retrieval.entities list included some cache reasons but omitted `error` (and `collapsed`, `stale_time`), making it inconsistent with the canonical 9-reason table. List all nine reason slugs, ordered to mirror the table. Low-risk: the "error" token was already in the lexical index via stale_error, and injection is gated on exact score — verified generic error prompts ("fix this null pointer error", etc.) still inject nothing, while MISS/STALE/BYPASS/hit-rate prompts still route to cdn-caching. Co-Authored-By: Claude Opus 4.8 (1M context) --- generated/skill-catalog.md | 2 +- generated/skill-manifest.json | 11 +++++++---- skills/cdn-caching/SKILL.md | 9 ++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index 78deea1..ebc3e85 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,7 +1,7 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-06T19:39:35.006Z +> Generated: 2026-07-06T20:19:33.040Z > Skills: 30 ## Table of Contents diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index 7fcee90..06da690 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-06T19:39:34.292Z", + "generatedAt": "2026-07-06T20:19:33.486Z", "version": 2, "skills": { "ai-gateway": { @@ -934,12 +934,15 @@ ], "entities": [ "cacheReason", - "stale_tag", - "stale_error", + "cold", + "collapsed", + "error", "draft_mode", "prerender_bypass", "crawler", - "cold" + "stale_time", + "stale_tag", + "stale_error" ], "examples": [] } diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index aa0a945..8191e04 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -43,12 +43,15 @@ retrieval: - why was this a cache miss entities: - cacheReason - - stale_tag - - stale_error + - cold + - collapsed + - error - draft_mode - prerender_bypass - crawler - - cold + - stale_time + - stale_tag + - stale_error chainTo: - pattern: 'use cache|cacheLife|cacheTag' From c65745e2466c5bb9672dd9bc731e38e952d806b9 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Mon, 6 Jul 2026 14:21:16 -0600 Subject: [PATCH 5/9] [skills] Restore docs/caching/cdn-cache link dropped during trim The cdn-cache docs URL was inadvertently removed while tightening the docs list; it's the canonical CDN-cache reference for this skill. Restore it alongside the caching overview, ISR, metrics, and logs links. Co-Authored-By: Claude Opus 4.8 (1M context) --- generated/skill-catalog.md | 2 +- generated/skill-manifest.json | 3 ++- skills/cdn-caching/SKILL.md | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index ebc3e85..fbbc097 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,7 +1,7 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-06T20:19:33.040Z +> Generated: 2026-07-06T20:21:06.015Z > Skills: 30 ## Table of Contents diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index 06da690..bf47ede 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-06T20:19:33.486Z", + "generatedAt": "2026-07-06T20:21:06.456Z", "version": 2, "skills": { "ai-gateway": { @@ -854,6 +854,7 @@ "summary": "", "docs": [ "https://vercel.com/docs/caching", + "https://vercel.com/docs/caching/cdn-cache", "https://vercel.com/docs/incremental-static-regeneration", "https://vercel.com/docs/cli/metrics", "https://vercel.com/docs/cli/logs" diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index 8191e04..9b895b6 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -5,6 +5,7 @@ metadata: priority: 6 docs: - 'https://vercel.com/docs/caching' + - 'https://vercel.com/docs/caching/cdn-cache' - 'https://vercel.com/docs/incremental-static-regeneration' - 'https://vercel.com/docs/cli/metrics' - 'https://vercel.com/docs/cli/logs' From 32607fcc3259e13c0812c25fa5fc528b733c8e19 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Mon, 6 Jul 2026 14:26:29 -0600 Subject: [PATCH 6/9] [skills] Redo cacheReason as a small additive change to cdn-caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior commits had rewritten (gutted) the existing cdn-caching skill. Restore the original #108 skill verbatim and add only what's needed: - A 9-row cacheReason table under Key concepts (3 MISS: cold/collapsed/ error, 3 BYPASS: draft_mode/prerender_bypass/crawler, 3 STALE: stale_time/stale_tag/stale_error), the MISS→BYPASS displayed-status rule, and how to read cacheReason (vercel logs / Logs panel; the x-vercel-cache-reason header is internal-only). - Additive promptSignals + a retrieval block so cache-reason questions route here. Net diff vs the original: +47 / -2 lines. All existing metrics/ISR/BYPASS runbook content preserved. 899 tests pass; routing verified via the real UserPromptSubmit hook. Co-Authored-By: Claude Opus 4.8 (1M context) --- generated/build-from-skills.manifest.json | 2 +- generated/skill-catalog.md | 2 +- generated/skill-manifest.json | 30 +-- skills/cdn-caching/SKILL.md | 231 +++++++++++++++++----- 4 files changed, 198 insertions(+), 67 deletions(-) diff --git a/generated/build-from-skills.manifest.json b/generated/build-from-skills.manifest.json index b56fbbe..a874b64 100644 --- a/generated/build-from-skills.manifest.json +++ b/generated/build-from-skills.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-06T19:39:32.893Z", + "generatedAt": "2026-07-06T20:25:44.559Z", "templates": [ { "template": "agents/ai-architect.md.tmpl", diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index fbbc097..09dcdee 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,7 +1,7 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-06T20:21:06.015Z +> Generated: 2026-07-06T20:25:46.677Z > Skills: 30 ## Table of Contents diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index bf47ede..d84614a 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-06T20:21:06.456Z", + "generatedAt": "2026-07-06T20:25:45.953Z", "version": 2, "skills": { "ai-gateway": { @@ -856,8 +856,7 @@ "https://vercel.com/docs/caching", "https://vercel.com/docs/caching/cdn-cache", "https://vercel.com/docs/incremental-static-regeneration", - "https://vercel.com/docs/cli/metrics", - "https://vercel.com/docs/cli/logs" + "https://vercel.com/docs/cli/metrics" ], "pathPatterns": [], "bashPatterns": [ @@ -874,18 +873,20 @@ { "pattern": "use cache|cacheLife|cacheTag", "targetSkill": "next-cache-components", - "message": "Next.js cache directives detected — loading Cache Components guidance." + "message": "Next.js cache directives detected — loading Cache Components guidance for revalidate/tag tuning." } ], "promptSignals": { "phrases": [ "cache hit rate", "isr cost", + "isr read units", + "isr write units", "stale content", "x-vercel-cache", "cache reason", - "stale_tag", - "request collapsed" + "x-vercel-cache-reason", + "stale_tag" ], "allOf": [ [ @@ -896,6 +897,14 @@ "stale", "cache" ], + [ + "revalidation", + "count" + ], + [ + "cache", + "reason" + ], [ "why", "stale" @@ -907,17 +916,14 @@ [ "cache", "miss" - ], - [ - "cache", - "reason" ] ], "anyOf": [ "revalidate", + "prerender", "invalidate", - "crawler", - "cold cache" + "draft mode", + "crawler" ], "noneOf": [], "minScore": 6 diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index 9b895b6..a02d0c4 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -1,6 +1,6 @@ --- name: cdn-caching -description: Debug Vercel CDN caching — cache hit rate, stale content, revalidation, ISR + PPR, per-request cache reasons (cacheReason), and ISR cost. +description: Debug Vercel CDN caching — cache hit rate, stale content, revalidation behavior, ISR + PPR, per-request cache reasons (cacheReason), and costs. metadata: priority: 6 docs: @@ -8,30 +8,33 @@ metadata: - 'https://vercel.com/docs/caching/cdn-cache' - 'https://vercel.com/docs/incremental-static-regeneration' - 'https://vercel.com/docs/cli/metrics' - - 'https://vercel.com/docs/cli/logs' bashPatterns: - '\bvercel\s+cache\s+(purge|invalidate|dangerously-delete)\b' promptSignals: phrases: - 'cache hit rate' - 'isr cost' + - 'isr read units' + - 'isr write units' - 'stale content' - 'x-vercel-cache' - 'cache reason' + - 'x-vercel-cache-reason' - 'stale_tag' - - 'request collapsed' allOf: - [cache, debug] - [stale, cache] + - [revalidation, count] + - [cache, reason] - [why, stale] - [why, bypass] - [cache, miss] - - [cache, reason] anyOf: - 'revalidate' + - 'prerender' - 'invalidate' + - 'draft mode' - 'crawler' - - 'cold cache' minScore: 6 retrieval: aliases: @@ -57,81 +60,203 @@ chainTo: - pattern: 'use cache|cacheLife|cacheTag' targetSkill: next-cache-components - message: 'Next.js cache directives detected — loading Cache Components guidance.' + message: 'Next.js cache directives detected — loading Cache Components guidance for revalidate/tag tuning.' --- # Vercel Caching -Expert guidance on Vercel's CDN Cache, ISR, and PPR: hit rate, stale content, revalidation, per-request cache reasons, and ISR cost. ISR/PPR are framework features (Next.js, SvelteKit, Nuxt, Astro) — the layers, metrics, and CLI below apply to all. +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 `prerender` HIT). + +### 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](https://vercel.com/docs/functions/configuring-functions/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. -## How caching works +**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. -A request reaches the nearest PoP → a Vercel region; the CDN checks each layer in order and returns the first cached copy, so your function runs only on a full miss. +#### Key concepts -- **CDN cache** — regional, ephemeral, free reads/writes. A HIT returns with no function call. -- **ISR cache** — durable, single region. Read on a CDN miss before invoking your function (shielding); billed in 8 KB units; survives deploys 31 days or until revalidated. -- **Function** — runs only if neither cache has a valid copy; its result is stored in ISR. -- **Request collapsing** — concurrent requests to one uncached path collapse into a single origin invocation per region. -- **PPR** — the static shell lives in the ISR cache; the function fills dynamic holes per request. A holeless route is plain ISR (a `prerender` HIT). +- **Cache hit rate** — share served from cache (`HIT`/`STALE`/`PRERENDER`) versus origin (`MISS`/`REVALIDATED`). Measure it over _cacheable_ requests — exclude `BYPASS` and `(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.js `revalidateTag`/`revalidatePath`) = stale-while-revalidate. Keeps serving stale while refreshing in the background → response shows `x-vercel-cache: STALE`. + - _Dangerously-delete_ (`dangerouslyDeleteByTag`, Next.js `updateTag` or 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-cache` response header) — the _outcome_: -## Cache status vs. cache reason + | 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.) | -**Status** (`x-vercel-cache`) is the _outcome_; **reason** (`cacheReason`, in logs) is _why_. The reason is the only way to tell three `STALE`s — or three `MISS`es — apart, since the `cache_result` metric lumps them together. +- **Cache reason** (`cacheReason`) — the finer _explanation_ of that outcome for a single request. The `cache_result` metric lumps all `MISS`es (and all `STALE`s) together; the reason is the only thing that tells them apart. Nine values, three per group: -| Status | Meaning | -| --- | --- | -| `HIT` | Served from cache; no function ran | -| `MISS` | Not cached; origin/function ran | -| `STALE` | Served stale while revalidating in background (SWR) | -| `PRERENDER` | Served a prerendered ISR/PPR shell | -| `REVALIDATED` | Foreground regen after a delete (or `Pragma: no-cache`) | -| `BYPASS` | Caching skipped (`no-store`, `private`, cookies, etc.) | + | `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) | -| `cacheReason` | Refines | Meaning | -| --- | --- | --- | -| `cold` | MISS | Cache empty for this key/variant (first request or evicted) | -| `collapsed` | MISS | Concurrent requests collapsed into one origin 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 (SWR) | -| `stale_tag` | STALE | Tag invalidated (`revalidateTag`/`invalidateByTag`); regenerating | -| `stale_error` | STALE | Revalidation **failed**; serving 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`. -A raw `MISS` with `draft_mode` / `prerender_bypass` / `crawler` is **displayed as `BYPASS`** (all usually expected). The `stale_*` reasons separate a healthy time refresh (`stale_time`) from a broad-tag blast (`stale_tag`) from a failing regen (`stale_error`). +## Investigating cache issues -## Investigating +Reach for the Vercel CLI. `vercel metrics` gives aggregate numbers (requires [Observability Plus](https://vercel.com/docs/observability/observability-plus)); `vercel logs` shows per-request behavior. -`vercel metrics` gives aggregates (needs Observability Plus); `vercel logs` shows per-request behavior. Query metrics by `-S -p `, filter prod with `-f "environment eq 'production'"`, add `-F json` for machine output. +Metrics need to be queried by team and project (`-S -p `). Filter production with `-f "environment eq 'production'"` (there is no `--prod` flag). Run `vercel metrics schema ` to discover dimensions; use `-F json` for machine-readable output. With `-g`, remember **`--limit` is per time bucket** — omit `-g` when you need totals across the whole window. -- **Hit rate** — group `vercel.request.count` by `cache_result` (HIT/STALE/PRERENDER = served; focus `MISS`; exclude `BYPASS`). Split `MISS` by `path_type`, then `request_path`. -- **ISR cost** — focus `vercel.isr_operation.write_units` (charged on every regen); the CDN shields ISR so `read_units` run far below request count. Group write_units by `cache_tags` — unrelated routes with near-identical counts mean a shared broad tag firing in lockstep. Confirm by grepping the `revalidateTag(` / `invalidateByTag(` / `updateTag(` call site. -- **BYPASS** — mostly Draft Mode + crawlers (expected); group by `bot_category` / `user_agent` to see what's left. Manage bots with the `vercel-firewall` skill. +### Cache hit rate + +Start here for an overall picture of how well caching is working. + +**Step 1 — overall split.** Group `vercel.request.count` by `cache_result`. Treat `HIT`, `STALE`, and `PRERENDER` as cache-served; focus investigation on `MISS`. Exclude `BYPASS` and `(not set)` when computing a hit rate over _cacheable_ traffic (see [Debugging BYPASS traffic](#debugging-bypass-traffic)). `STALE` means stale-while-revalidate is working — dig into revalidation frequency in [Analyzing ISR costs](#analyzing-isr-costs), not here. ```bash -vercel metrics vercel.request.count -S -p --group-by cache_result --since 24h -vercel metrics vercel.isr_operation.write_units -S -p -a sum --group-by cache_tags --since 24h +vercel metrics vercel.request.count -S -p \ + -f "environment eq 'production'" --group-by cache_result --since 24h ``` -**One request** — `curl -sSI ` shows `x-vercel-cache`, `x-matched-path` (reveals experiment precompute), `vary`, and `set-cookie` (forces BYPASS). For the reason, read logs — the `x-vercel-cache-reason` header is internal-only and not visible via curl: +**Step 2 — where misses concentrate.** Split the `MISS` bucket (and optionally `STALE`) by `path_type`, then by `route` or `request_path`: ```bash -vercel logs --json | jq -r 'select(.cacheReason!="") | .cacheReason' | sort | uniq -c | sort -rn +vercel metrics vercel.request.count -S -p \ + -f "environment eq 'production' and cache_result eq 'MISS'" \ + --group-by path_type --since 24h + +vercel metrics vercel.request.count -S -p \ + -f "environment eq 'production' and cache_result eq 'MISS' and path_type eq 'prerender'" \ + --group-by request_path --since 24h ``` +**What to expect:** `prerender` routes (static shells, ISR pages) should show a high share of `HIT`/`PRERENDER`. A `prerender` path with a disproportionate `MISS` count is your short list for per-path header inspection (`curl` above) and code review. + +`streaming_func` routes render dynamically by default, but you can still cache them with `Cache-Control` headers — matching requests are cached on the CDN. Each cache entry varies by `Vary` headers (cookies, RSC, etc.) as well as path and query parameters, so expect more cache keys and a lower hit rate than a fully static `prerender` route. + +### Analyzing ISR costs + +Once you know hit rate, quantify ISR spend and whether revalidation — not traffic volume — is driving it. + +**Utilization vs. ISR billing.** **Utilization** is `vercel.request.count` — total request volume. **ISR cost** is billed separately in 8 KB units: `read_units` when the regional CDN misses and falls through to the ISR cache, and `write_units` on every revalidation/regeneration. The regional CDN shields ISR heavily — most requests never touch the ISR layer, so **read_units will be far below request count**. Do not compare read_units to write_units as a utilization check; focus on **write_units** (revalidation cost) and how they relate to total traffic. + +```bash +vercel metrics vercel.request.count -S -p -a sum --since 24h +vercel metrics vercel.isr_operation.write_units -S -p -a sum --since 24h +``` + +**Which routes revalidate most.** Break write units down by `route` and `request_path` to find paths that regenerate often relative to traffic: + +```bash +vercel metrics vercel.isr_operation.write_units -S -p \ + -a sum --group-by route --since 24h + +vercel metrics vercel.isr_operation.write_units -S -p \ + -a sum --group-by request_path --since 24h +``` + +**Regeneration vs. serving.** Group write units by `path_type` — concentration in `background_func` confirms revalidation (not per-request dynamic work) is the cost driver. + +**Time-based vs. tag-based revalidation.** Time-based intervals regenerate on a schedule whether or not content changed — often inefficient. Tag-based on-demand revalidation is usually better, but an **overly broad tag** has a large blast radius: one invalidate drops every entry that carries it. + +- **Tag blast radius** — group write units by `cache_tags`. If many _unrelated_ routes show near-identical write counts, a shared hot tag is invalidating them in lockstep (e.g. every blog post rewriting at the same rate because they share one broad `blogPost` tag): + +```bash +vercel metrics vercel.isr_operation.write_units -S -p \ + -a sum --group-by cache_tags --since 24h +``` + +- **What triggered revalidation** — group `vercel.request.count` by `triggering_tag` to see which tags fire most often (`triggering_tag` is on request count only, not ISR operation metrics. It is one of the tags that triggered the page to be stale): + +```bash +vercel metrics vercel.request.count -S -p \ + -f "triggering_tag ne null" --group-by triggering_tag --since 24h +``` + +Tags with a large blast radius that revalidate frequently are the usual root cause of high write_units. Prefer granular tags (`product-${id}`) and on-demand invalidation over short time-based intervals for event-driven content. + +**Confirm in code.** Metrics tell you _which_ tag is hot; the repo tells you _why_. Grep for the tag's invalidation call site — `revalidateTag(`, `invalidateByTag(`, `updateTag(`, `dangerouslyDeleteByTag(` — and read the trigger. A CMS webhook or a sync cron that invalidates a **broad** tag on every event (instead of a specific `${type}:${id}`) is the classic amplifier. + +### Debugging BYPASS traffic + +The largest legitimate sources of `BYPASS` are **Draft Mode** and **SEO crawlers**. Draft Mode must bypass cache so editors see live content. SEO bots must receive the **full response** — especially on PPR routes where the static shell and dynamic holes are assembled at request time — so crawlers index what users actually see. That BYPASS is expected, not a misconfiguration. + +Before tuning headers or revalidate intervals, confirm what's left after those two buckets: + +```bash +vercel metrics vercel.request.count -S -p \ + -f "cache_result eq 'BYPASS'" --group-by bot_category --since 24h + +vercel metrics vercel.request.count -S -p \ + -f "cache_result eq 'BYPASS'" --group-by user_agent --since 24h + +vercel metrics vercel.request.count -S -p \ + -f "cache_result eq 'BYPASS'" --group-by request_method --since 24h +``` + +The **Firewall/WAF** with the `vercel-firewall` skill can be used to manage verified SEO crawlers, block abusive bots, and rate-limit junk traffic before it distorts your hit-rate picture. + ## Reducing ISR cost -- Prefer tag-based on-demand revalidation over short time intervals (which regen whether content changed or not). -- Scope tags to IDs (`product-${id}`), not broad `page` / `blogPost` tags with a large blast radius. -- Tune the revalidate interval where the framework declares it; for Next.js `use cache` / `cacheLife`, see `next-cache-components`. +- **Prefer tag-based over time-based revalidation.** Replace short `revalidate` intervals with on-demand `revalidateTag` / `invalidateByTag` when content changes — time-based regeneration runs whether or not anything changed. If using Cache Components, analyze `cacheLife` calls with the `next-cache-components` skill. +- **Scope tags to specific IDs.** Invalidate `blogPost:`, not a generic `blogPost`/`page` tag — one broad invalidate regenerates everything that carries it. +- Tune the revalidate interval where your framework declares it (Next.js `revalidate` / `cacheLife`, SvelteKit `isr`, Nuxt `routeRules`, Astro). For Next.js Cache Components, see the `next-cache-components` skill. +- Use `CDN-Cache-Control` headers to cache dynamic functions. + +### Inspect one path + +```bash +curl -sSI https:/// | grep -iE 'x-vercel-cache|x-matched-path|cache-control|vary|age|set-cookie' +``` + +This zero-dependency first reach shows the status (`x-vercel-cache`), the cache directives (`Cache-Control` / `CDN-Cache-Control` / `Vercel-CDN-Cache-Control`), and — crucially — **`x-matched-path`**, which reveals rewrites like `/precomputed/exp~.../...` that expose experiment/flag precomputation. `vary` flags personalization (RSC, cookies); `set-cookie` forces `BYPASS`. For a per-phase timing breakdown, `vercel httpstat /some/path` (CLI v48.9.0+; needs the `httpstat` tool installed) adds latency stats. A path that should cache but shows `MISS`/`BYPASS` usually has `private`, `no-store`, `max-age=0`, a per-request input (cookies/headers/`searchParams`), or an uncacheable method (see FAQ). + +**Inspect one request.** When metrics or headers give you a request ID, pull the full log record: + +```bash +vercel logs --request-id --json +``` + +Use `--json` so the agent can parse cache status, path, and timing fields programmatically. + +## FAQ + +- **What are prerender variant misses?** When a route uses a dynamic param, each distinct cache-key variant is prerendered and cached separately, so each variant misses on its first hit per region and low-traffic ones rarely stay warm. The most common modern cause is **feature-flag / experiment precomputation** — middleware picks a variant per request (`/precomputed/exp~.../...` paths), and flags × routes × PPR segments multiply into thousands of ISR entries (also a middleware-invocation cost). Fix: collapse the variant matrix (retire finished experiments), or accept the cost. +- **Does PPR avoid function invocations?** No — a PPR route has dynamic holes by definition, so the cached shell hit still runs the function to fill them. (A route with _no_ holes is just ISR and serves a pure `prerender` HIT — see Key concepts.) +- **Why are there more function invocations than PPR requests?** PPR requests have a static shell and a dynamic function invocation. When the static shell needs to be regenerated, it incurs a function invocation on top of the dynamic function for the content. ## Related skills -- `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, `revalidate` tuning. -- `runtime-cache` — per-region key-value cache between a function and a backend. -- `vercel-firewall` — verified crawlers, bot blocking, rate limits. +- `vercel-firewall` — manage verified SEO crawlers, block abusive bots, and rate-limit junk BYPASS traffic. +- `runtime-cache` — caching data _between your function and a backend_ (per-region key-value / data cache). A different layer from the CDN/ISR caches; use it to cache an API response or query result inside a function. +- `next-cache-components` — Next.js `use cache`, `cacheLife`, `cacheTag`, and `revalidate` tuning (one framework's ISR/PPR controls). -## References +## References: -- https://vercel.com/docs/caching · /incremental-static-regeneration · /partial-prerendering · /cli/metrics · /cli/logs +- Caching overview: https://vercel.com/docs/caching +- ISR: https://vercel.com/docs/incremental-static-regeneration +- Partial Prerendering (PPR): https://vercel.com/docs/partial-prerendering +- Cache-Control headers: https://vercel.com/docs/caching/cache-control-headers +- Diagnosing and fixing cache issues (full runbook): https://vercel.com/docs/caching/cdn-cache/debug-cache-issues +- vercel metrics CLI: https://vercel.com/docs/cli/metrics +- vercel logs CLI: https://vercel.com/docs/cli/logs From 1f6c0004f356ef92f1e87961ca2eff3c7b3fe266 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Mon, 6 Jul 2026 14:39:16 -0600 Subject: [PATCH 7/9] [skills] Make cacheReason prompt signals consistent across reasons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only stale_tag was a phrase. Add the other distinctive reason slugs so pasting any of them from a log triggers the skill: stale_time, stale_error, draft_mode, prerender_bypass (+ existing stale_tag). Add 'cold cache' and 'request collapsed' to anyOf for MISS coverage. Bare cold/collapsed/error are intentionally NOT phrases — as ordinary English words they would false-match unrelated prose. Verified: "cacheReason: stale_error" and "prerender_bypass" fire cdn-caching, while "fix this null pointer error" / "handle the error case" inject nothing. Co-Authored-By: Claude Opus 4.8 (1M context) --- generated/build-from-skills.manifest.json | 2 +- generated/skill-catalog.md | 2 +- generated/skill-manifest.json | 12 +++++++++--- skills/cdn-caching/SKILL.md | 6 ++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/generated/build-from-skills.manifest.json b/generated/build-from-skills.manifest.json index a874b64..a68df91 100644 --- a/generated/build-from-skills.manifest.json +++ b/generated/build-from-skills.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-06T20:25:44.559Z", + "generatedAt": "2026-07-06T20:39:10.921Z", "templates": [ { "template": "agents/ai-architect.md.tmpl", diff --git a/generated/skill-catalog.md b/generated/skill-catalog.md index 09dcdee..dab6bb9 100644 --- a/generated/skill-catalog.md +++ b/generated/skill-catalog.md @@ -1,7 +1,7 @@ # Skill Catalog > Auto-generated by `scripts/generate-catalog.ts` — do not edit manually. -> Generated: 2026-07-06T20:25:46.677Z +> Generated: 2026-07-06T20:39:13.075Z > Skills: 30 ## Table of Contents diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index d84614a..29b01dc 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-06T20:25:45.953Z", + "generatedAt": "2026-07-06T20:39:12.336Z", "version": 2, "skills": { "ai-gateway": { @@ -886,7 +886,11 @@ "x-vercel-cache", "cache reason", "x-vercel-cache-reason", - "stale_tag" + "stale_time", + "stale_tag", + "stale_error", + "draft_mode", + "prerender_bypass" ], "allOf": [ [ @@ -923,7 +927,9 @@ "prerender", "invalidate", "draft mode", - "crawler" + "crawler", + "cold cache", + "request collapsed" ], "noneOf": [], "minScore": 6 diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index a02d0c4..dca6f2d 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -20,7 +20,11 @@ metadata: - 'x-vercel-cache' - 'cache reason' - 'x-vercel-cache-reason' + - 'stale_time' - 'stale_tag' + - 'stale_error' + - 'draft_mode' + - 'prerender_bypass' allOf: - [cache, debug] - [stale, cache] @@ -35,6 +39,8 @@ metadata: - 'invalidate' - 'draft mode' - 'crawler' + - 'cold cache' + - 'request collapsed' minScore: 6 retrieval: aliases: From efc65a8604a812ddf78f8fc96ca512626c2cd574 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Tue, 7 Jul 2026 10:33:40 -0700 Subject: [PATCH 8/9] [skills] Route the cacheReason field name to cdn-caching --- generated/build-from-skills.manifest.json | 2 +- generated/skill-manifest.json | 3 ++- skills/cdn-caching/SKILL.md | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/generated/build-from-skills.manifest.json b/generated/build-from-skills.manifest.json index a68df91..101a605 100644 --- a/generated/build-from-skills.manifest.json +++ b/generated/build-from-skills.manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-06T20:39:10.921Z", + "generatedAt": "2026-07-07T17:26:07.851Z", "templates": [ { "template": "agents/ai-architect.md.tmpl", diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index e9ff2ca..eaf2e56 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-06T20:39:12.336Z", + "generatedAt": "2026-07-07T17:34:16.360Z", "version": 2, "skills": { "ai-gateway": { @@ -885,6 +885,7 @@ "stale content", "x-vercel-cache", "cache reason", + "cacheReason", "x-vercel-cache-reason", "stale_time", "stale_tag", diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index dca6f2d..9aae1a3 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -19,6 +19,7 @@ metadata: - 'stale content' - 'x-vercel-cache' - 'cache reason' + - 'cacheReason' - 'x-vercel-cache-reason' - 'stale_time' - 'stale_tag' From 6f104ceee66813d8b58de09c4dc2862590d3eab0 Mon Sep 17 00:00:00 2001 From: Shina Patel Date: Tue, 7 Jul 2026 10:36:57 -0700 Subject: [PATCH 9/9] [skills] Drop generic single-word cache-reason entities (cold, error, crawler) per review --- generated/skill-manifest.json | 5 +---- skills/cdn-caching/SKILL.md | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/generated/skill-manifest.json b/generated/skill-manifest.json index eaf2e56..a5fd420 100644 --- a/generated/skill-manifest.json +++ b/generated/skill-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-07T17:34:16.360Z", + "generatedAt": "2026-07-07T17:36:42.694Z", "version": 2, "skills": { "ai-gateway": { @@ -948,12 +948,9 @@ ], "entities": [ "cacheReason", - "cold", "collapsed", - "error", "draft_mode", "prerender_bypass", - "crawler", "stale_time", "stale_tag", "stale_error" diff --git a/skills/cdn-caching/SKILL.md b/skills/cdn-caching/SKILL.md index 9aae1a3..1665887 100644 --- a/skills/cdn-caching/SKILL.md +++ b/skills/cdn-caching/SKILL.md @@ -54,12 +54,9 @@ retrieval: - why was this a cache miss entities: - cacheReason - - cold - collapsed - - error - draft_mode - prerender_bypass - - crawler - stale_time - stale_tag - stale_error