Request Lifecycle
This page is the technical deep-dive for how each request is processed.
Request lifecycle
Every HTTP request that reaches the enforcement point goes through the following steps in order.
1. Context extraction
build_request_context() assembles a normalized view of the request:
- Host — request host used by selector
hostsfiltering - Headers — stored with three key variants per header: as-is, underscore form, and hyphen form, so
X-API-Key,x_api_key, andx-api-keyall resolve correctly - JWT payload — the
Authorization: Bearer <token>header is base64url-decoded (payload only, no signature verification) - IP address — taken from
ngx.var.remote_addr - User-Agent — read lazily only when active policy keys require
ua:*descriptors - Query parameters — parsed from
ngx.var.query_string
2. Bundle check
If no bundle is loaded yet (edge is still starting), the request is rejected with 503 Service Unavailable and reason no_bundle_loaded.
3. Runtime overrides and kill switch scan
Before the kill-switch scan, runtime override blocks are checked:
global_shadowactive -> all matched policies are treated as shadow modekill_switch_overrideactive -> kill-switch scan is skipped
If kill_switch_override is not active, kill switches are evaluated before any route matching.
The engine scans every entry in bundle.kill_switches:
- If the entry has
expires_atand that time is past, the entry is skipped. - The descriptor for the entry’s
scope_keyis extracted from the request context. - If the descriptor value equals
scope_value(exact match), and the optionalroutefield also matches — the request is immediately rejected withreason: kill_switchand aRetry-After: 3600header.
First match wins.
4. Route matching
route_index.match(host, method, path) matches selectors in two stages:
- Host filter (
selector.hosts) is evaluated first. pathPrefixselectors collect matches at every level of the trie.pathExactselectors match only at the leaf node.methodsfilters are applied per-policy after trie traversal.
If no policies match, the request is allowed with reason: no_matching_policy.
5. Per-policy evaluation
For each matched policy (in index order):
5a. Shadow mode detection
If policy.spec.mode == "shadow" or top-level global_shadow is active, limiter state for this policy is namespaced under a shadow: prefix. A would-reject is recorded but traffic is never blocked.
5b. Loop detection
If loop_detection.enabled, a fingerprint is computed from request inputs and descriptors. A counter is incremented in shared dict with TTL = window_seconds. If the counter exceeds threshold_identical_requests, the configured action (reject / throttle / warn) is applied.
5c. Circuit breaker
If circuit_breaker.enabled, spend rate for the policy key is checked. If breaker is open, request is rejected with reason: circuit_breaker_open.
5d. Rule matching
Rules are evaluated in definition order. Each rule may have a match block that filters by descriptor equality. If no rules match and fallback_limit exists, fallback is used.
5e. Per-rule limiter
The algorithm in rule.algorithm is invoked:
| Algorithm | Module | State key prefix |
|---|---|---|
token_bucket |
token_bucket.lua |
tb: |
cost_based |
cost_budget.lua |
cb: |
token_bucket_llm |
llm_limiter.lua |
tpm: / tpd: |
All limiters use ngx.shared.dict (fairvisor_counters) for state.
If any limiter denies the request, evaluation stops and reject is returned.
6. Allow or reject
- Allow:
200 OK(decision service) or pass-through (reverse proxy). Rate-limit headers may be set. - Reject:
429 Too Many Requestswith:X-Fairvisor-ReasonRetry-After(deterministic per-identity jitter)RateLimit,RateLimit-Reset- optionally
RateLimit-Limit,RateLimit-Remaining
- Throttle: worker sleeps
delay_ms(max 30s), then allows.
Policy/rule attribution is available via debug-session headers (X-Fairvisor-Debug-*), not standard reject headers.
Fail-open semantics
| Condition | Behaviour |
|---|---|
| No bundle loaded | 503 (intentional; misconfiguration signal) |
ngx.shared.dict write error |
Allow (logged as warning) |
| Missing descriptor key | Rule is skipped; warning logged |
| JSON parse error in body | Token estimation falls back to body_length / 4 |
| SaaS unreachable | Last known bundle remains active |
Evaluation order summary
Request in
-> Runtime overrides check
-> kill_switch_override active? skip kill-switch scan
-> Kill switches (first match -> 429, when enabled)
-> Route index match (host + method + path)
-> no match -> allow
-> For each matched policy:
-> loop detection (optional)
-> circuit breaker (optional)
-> for each matching rule:
-> limiter check
-> deny -> 429 (or shadow-allow)
-> allow -> continue
-> all policies passed -> allow