<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->
<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->
<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->

# Candidate gates

The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.

Total gates: 15. Budget cap: `MAX_CODE_CANDIDATES = 6`. Gate version: `1.8.0`.

## Gates

### `build_minutes_fanout`

- **Threshold**: `Build Minutes share > 0.15 OR turbo-force-bypass finding present`
- **Billing dimension**: build
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`

Build Minutes line dominates the bill or Turborepo cache is bypassed. On monorepos, unchanged work should be skipped through Vercel skip-unaffected behavior, a verified Ignored Build Step, and a complete Turbo cache contract.

---

### `cold_start`

- **Threshold**: `coldPct > 0.4 AND total >= 1000`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`

Routes where > 40% of invocations are cold-start, at meaningful traffic (>=1,000 total invocations in window). Cold starts add 200-800ms per request and break the perceived latency budget on cache-miss paths. The 40% threshold is where cold-rate becomes a real signal vs Poisson noise on serverless. Sourced from vercel.function_invocation.count grouped by function_start_type.

---

### `cwv_poor`

- **Threshold**: `LCP p75>2500 OR INP p75>200 OR CLS p75>0.1, AND speed_insights count > 50`
- **Billing dimension**: speed-insights
- **Scope**: route
- **Source citation**: `https://web.dev/articles/vitals`

Routes where Core Web Vitals fall into Google's "Poor" band on real-user traffic. LCP > 2500ms, INP > 200ms, or CLS > 0.1 each hurt SEO and conversion. Surfaces one candidate per (route, metric) pair to keep recommendations focused.

---

### `external_api_slow`

- **Threshold**: `p75Ms > 2000 AND callCount >= 500`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`

External API hostnames with p75 latency above 2 seconds AND at least 500 calls in the window. External API latency is a primary driver of function duration cost when the upstream is on a hot path; a single slow stale call isn't worth recommending against.

---

### `isr_overrevalidation`

- **Threshold**: `writes/reads > 0.5 AND writes > 100`
- **Billing dimension**: isr
- **Scope**: route
- **Source citation**: `https://vercel.com/docs/incremental-static-regeneration`

ISR routes with > 1 write per 2 reads. The revalidate interval is too aggressive relative to read traffic — many reads pay to regenerate. Investigate whether the page can tolerate a longer revalidate window or on-demand revalidation via revalidateTag.

---

### `middleware_heavy`

- **Threshold**: `middlewareInv/totalInv > 0.5 AND middlewareInv > 1000`
- **Billing dimension**: edge-requests
- **Scope**: account
- **Source citation**: `https://nextjs.org/docs/app/building-your-application/routing/middleware`

Middleware invocations cover > 50% of total requests at non-trivial volume. The matcher is probably broader than necessary; narrow it to the paths that actually need auth/rewrites/headers.

---

### `observability_events_attribution`

- **Threshold**: `observabilityEventsShare > 0.20 (critical at > 0.30)`
- **Billing dimension**: observability-events
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`

Observability Events line item exceeds 20% of total billed cost. High share usually traces to low cache hit rate, middleware-heavy traffic, or unconstrained custom-span cardinality. No sampling lever exists for Observability Plus; reduce upstream invocations instead.

---

### `platform_bot_protection`

- **Threshold**: `botIdEnabled=false AND (botPct >= 0.05 OR edge_cost >= $25/window OR requests >= 14k/14d)`
- **Billing dimension**: edge-requests
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`

When BotID is disabled AND there is evidence (observed bot bandwidth share, edge cost, or substantial request volume) that bot traffic is non-trivial. Bot traffic inflates edge request counts without delivering user value; staged bot protection can reduce waste on bot-heavy projects. Skipped on quiet projects with no bot evidence — the recommendation would be noise.

---

### `platform_fluid_compute`

- **Threshold**: `fluid=false AND (any cold_start signal OR any route with p95>1000ms AND inv>1000)`
- **Billing dimension**: function-duration
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`

When Fluid Compute is disabled on a project that shows cold-start pressure (high cold-start rate) or sustained slow function p95 on hot routes. Fluid Compute reduces cold starts via instance reuse — recommend turning it on at the project level rather than per-route.

---

### `region_misconfig`

- **Threshold**: `single-region pin found AND routes.length > 20 (scanner-only branch)`
- **Billing dimension**: function-duration
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`

A single function region is pinned in `vercel.json` or per-route `preferredRegion`. Without per-region TTFB data (data gap), the gate can't quantify the geographic latency cost — but a single-region pin on a project with 20+ routes is worth auditing against Speed Insights traffic geo.

---

### `route_errors`

- **Threshold**: `count > 250 OR (totalRequests >= 1000 AND errorRate > 0.01)`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`

Routes producing > 250 5xx errors over the window, or with > 1% error rate on at least 1,000 total requests. Errored function invocations still bill at full duration; high error rates also poison user experience.

---

### `scanner-driven`

- **Threshold**: `per-kind: scanner matches.length >= threshold`
- **Billing dimension**: mixed
- **Scope**: mixed
- **Source citation**: `vercel-optimize gate threshold`

Configured kinds emitted from scanner output. Each requires a minimum match count to avoid noise. Findings on cold-path or unmappable files are dropped unless the underlying scanner is trafficIndependent.

---

### `slow_route`

- **Threshold**: `(p95 > 500 AND inv >= 1400) OR (p95 > 1500 AND inv >= 250); disqualified when 5xx rate > 50%; Vercel Workflow runtime endpoints are hard-gated`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`

Routes with p95 function duration above 500ms at meaningful traffic (>=1,400 invocations in window), OR catastrophically slow routes (>1500ms p95 at any volume >=250). High duration drives both function-duration cost and user-perceived latency. Investigate sequential awaits, slow external APIs, missing caching, N+1 patterns. Routes with >50% 5xx rate are disqualified — those are reliability problems, not performance tuning targets, and surface via route_errors instead. Vercel Workflow runtime endpoints (`/.well-known/workflow/v1/*`) are hard-gated before launch because long-running step/flow requests are expected orchestration, not app-route bottlenecks.

---

### `uncached_route`

- **Threshold**: `requests > 500 AND hitRate < 0.5 AND getShare > 0.2 (missing getShare is gated)`
- **Billing dimension**: edge-requests
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`

Routes serving > 500 requests/period at < 50% cache hit AND at least 20% GET traffic. Each uncached GET request reaches the function, costing edge requests + function duration. Routes that are mostly POST/PUT/DELETE (Server Actions, mutations) are skipped — 0% cache is correct behavior there. Routes with missing method-share data are gated instead of launched. Auth-gated routes are disqualified separately.

---

### `usage_spike_triage`

- **Threshold**: `any-day total > 2x mean OR any-day SKU > 3x SKU mean`
- **Billing dimension**: mixed
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`

A single day in the billing window deviates sharply from the window baseline. Triage branches: bot or AI crawler spike, viral moment, pricing-model migration (legacy SKU → new), code regression. Without daily-granularity data, this gate stays dormant.

---
