API request guard pipeline
Every API request to Orbit walks an ordered chain of per-request guards before it reaches a route handler. One request can fail for many reasons — a denylisted IP, a saturated event loop, a spent rate-limit bucket, a duplicate idempotency key — and the guards are sequenced so the cheapest, most defensive checks reject first. When you know which guard fired and in what order, a403/429/503 stops being a support ticket and becomes a
reading exercise on response headers and the error envelope.
This page maps the chain, the signal each guard returns, and how to read
the order in the code. For per-endpoint limit tables see
Rate limits; for the full limiter taxonomy see
Rate-limit and cooldown taxonomy.
Why a chain matters
Guard ordering fixes three questions that otherwise arrive as support tickets:- Which guard fired? A 403 from the IP denylist is a block; a 403 from your org-level allowlist is a misconfiguration. The order tells you which surface rejected the request before auth ever ran.
- What got skipped? A rejected request never reaches later guards — a denylist 403 never consumed rate-limit quota, and a 429 shows the idempotency guard was skipped on that attempt.
- Where is the fix? Each guard names a distinct owner. Denylist and allowlist entries are operator-curated; rate-limit buckets are your plan’s per-endpoint tables; the under-pressure shed is platform-side and retryable.
The ordered guard chain
The table below lists every guard in registration order. Guards marked terminal end the request when they fire; the rest annotate the request for downstream consumers.
Two consequences fall out of the ordering:
- 429 before 401 means auth effects are never wasted. The rate
limiter runs at
preHandler, keying on the API-key header or client IP, before auth populates the request context — so a throttled client never consumes auth/tenant work it would have failed anyway. - The denylist rejects cheaper than rate-limit. A platform-denied IP is rejected in guard 3, before any Redis-side bucket decrement — which is why a denylisted source can hammer the API forever without ever spending quota.
Per-guard triggers, status, and response envelope
Every terminal guard returns the error envelope the error-handler registers last — anerror object plus a meta block carrying
request_id and docs_url. You can correlate a guard’s request_id
against your own logs even when the guard fired before any handler ran.
The same
403 status fires from both the platform denylist and the per-org
allowlist. If you get a 403 with no allowlist configured on the org, treat
it as the platform denylist and contact support; if the org has an
allowlist, fix the list first.Where to read the order in code
The chain is registered in one block — the biggest part of the sequence sits inapps/api/src/app.ts between the first clientIpSocketPlugin
registration and the final errorHandler registration, with the
request-annotation plugins (request id, trace context, Sentry, metrics,
locale, logger, metrics, idempotency, policy scan, error handler) in one
contiguous block near the end of buildApp. The plugin implementations
live one-per-file in apps/api/src/plugins/, named after the guard they
run (retired-host-guard.ts, platform-ip-denylist-guard.ts,
under-pressure.ts, etag.ts, request-id.ts, trace-context.ts,
locale.ts, request-logger.ts, request-metrics.ts, idempotency.ts,
policy-scan-hook.ts, error-handler.ts).
Reading that block top-to-bottom is the definitive order; this page is the
map, the file is the territory.
Who owns each guard
All the envelope and ordering above is platform-owned plumbing — not tenant compliance controls. The tenant-owned controls sit behind auth (the org-level IP allowlist, per-endpointconfig.rateLimit
buckets on routes that opt out of the global limiter, idempotency-key
semantics you choose). See
Tenant isolation for the tenancy split and
Compliance for the tenant-ownership model.
See also
- Rate-limit and cooldown taxonomy — every limiter family and its error code
- Tenant isolation — the tenant-ownership split
- Idempotency and safe retries — the replay contract the idempotency guard enforces
- Rate limits — per-endpoint tables and retry patterns
- Error codes reference — the full error catalog and envelope shapes
- Request logs model — how the logger guard feeds the Developer API analytics