Skip to content
SeeItFlow
System Design · 9 chapters · 15 failure scenarios · ~28 min read

AI Gateway Failures: 429s, Streams & Recovery

Fifteen named failures, each walked through with what actually happens, why the naive version breaks, and the specific mechanism from Architecture Deep Dive that fixes or bounds it. The numbers throughout are illustrative planning figures, not measurements of any real system.

Mangesh MandlikLast updated: How lessons are made
Scenarios 1-2

Provider Capacity Failures

The upstream provider is the one dependency this design can't directly control — only react to, and isolate from.

Scenario 1 — Cascading 429s and the circuit-breaker recovery cycle

Swipe to pan
Cascading 429 TimelineProvider congestion → retries → breaker opens → traffic shifts → recovery probet+0sProvider A healthyt+2s6 consecutive 429st+2sCircuit OPENSt+2–32sAll traffic → Provider Bt+32sHalf-open probet+33sProbe OK → CLOSED
Provider congestion → retries → breaker opens → traffic shifts → recovery probe

t+0s: Provider A is healthy, serving most traffic for its logical models. t+2s: a burst of 6 consecutive 429s arrives from Provider A — could be a capacity event on the provider's side, unrelated to this gateway's own request rate. The circuit breaker counts the consecutive failures and opens. From t+2s to t+32s (a 30-second cooldown), every request that would have routed to Provider A instead routes to Provider B, with zero wasted attempts against the failing deployment. At t+32s, a single half-open probe request goes through; it succeeds, and the breaker closes at t+33s. Total user-visible impact: none, assuming Provider B had sufficient spare capacity to absorb the shifted load.

Detection

A consecutive-failure counter per deployment, exported as a metric, crossing the breaker's trip threshold (six in this design) is the detection signal — paired with a per-deployment error-rate dashboard so an operator sees the circuit open in real time rather than inferring it from a drop in Provider A's traffic share after the fact. Prevention is largely structural here: the breaker is the prevention mechanism, not a separate control — the real operational task is tuning the trip threshold and cooldown window against real provider failure patterns, and alerting if a breaker stays open longer than its cooldown would predict (a sign the provider outage is longer than transient).

Scenario 2 — Retry storm when both providers degrade simultaneously

A rarer, harder case: Provider A and Provider B both degrade at once (a shared upstream dependency, a regional network event, or simple coincidental timing). If every gateway replica's retry budget independently attempts both providers on every failure, the aggregate retry traffic can exceed the original request rate by 2-3x at the exact moment neither provider has spare capacity — a self-inflicted amplification on top of an external outage.

Retry budget as a global brake, not just a per-request setting

The fix is a gateway-wide retry budget — a rolling count of retries as a percentage of total requests (a common target is capping retries at 10% of request volume). Once the budget is exhausted, new failures return 503 immediately instead of attempting a retry, converting an amplifying failure into a flat, bounded one. See Retry & Backoff for the general retry-budget pattern.

Detection and prevention

Detection: total outbound request rate to both providers combined exceeding the admitted inbound request rate by a wide margin is the tell-tale sign of retry amplification — this ratio, not raw error count, is what should page someone. Prevention: capacity planning that assumes providers can correlate (a shared upstream region, a shared underlying model host) rather than treating 'two independent providers' as a guarantee of independent failure, and load-testing the retry budget's behavior specifically under simultaneous dual-provider degradation before it happens in production.

Scenarios 3-4

Quota & Budget Races

Scenario 3 — Local-counter double-admission

Two gateway replicas, each with an in-memory RPM/TPM counter instead of a shared one. With exactly one token-budget unit left for a tenant, two concurrent requests land on different replicas within the same millisecond; both check their local counter, both see headroom, both admit. The tenant's real TPM limit is exceeded by up to 2x in the worst case — and under enough concurrent replicas, by more.

Fix

Replace local counters with a shared atomic counter store (Architecture Deep Dive, Chapter 03) — the admission check and the decrement happen as one indivisible operation, so only one of two simultaneous requests for the last unit can succeed.

Detection and prevention

Detection: a tenant's measured TPM usage (summed from the ledger) exceeding its configured TPM limit is a direct, unambiguous signal that admission allowed more than it should have — this metric should be alerted on independently of any customer complaint. Prevention: a pre-production load test that deliberately races concurrent requests against the last unit of a tiny test quota is the standard way to catch a local-counter regression before it reaches production, since the bug is invisible at low concurrency.

Scenario 4 — Runaway daily spend from after-the-fact metering

Forty requests are in flight concurrently for one tenant near its daily budget edge. Each is individually checked against the budget only *after* completing — none of them know what the others are about to cost. By the time a dashboard reflects the overshoot, actual spend has already cleared the cap: an illustrative $612 against a $500 daily limit, a 22% overshoot caused entirely by the gap between 'checked' and 'reserved.'

Fix

Atomic budget reservation at admission (Architecture Deep Dive, Chapter 04) bounds this to the per-request reservation-vs-actual gap (typically single-digit percent, from estimate conservatism) rather than an unbounded number of concurrent requests all slipping through the same unenforced check.

Detection and prevention

Detection: a real-time 'reserved vs. hard cap' meter (not a post-hoc daily rollup) is what catches this while it's still happening — a dashboard that only updates once an hour will always show the overshoot after the fact. Prevention: reject new admissions once reserved-but-unsettled spend alone would exceed the cap, even before any of those reservations finalize — the reservation itself, not just the eventual settled total, counts against the limit.

Scenarios 5-7

Streaming Failures

Scenario 5 — Mid-stream provider disconnect

17 of an expected ~40 chunks have been delivered to the client when the provider connection drops — a TCP reset, a provider-side restart, doesn't matter which. The client has already rendered a partial answer. A naive implementation might try to silently fail over to Provider B and continue the response; the result would be a generation that switches models or restarts context mid-sentence, presented to the user as one continuous answer. That is not a transport bug — it is a factually false response.

Fix

An explicit stream_error event, carrying chunks-delivered and incomplete: true (Architecture Deep Dive, Chapter 09). The client can offer an explicit 'regenerate' action; the partial usage is still billed for what was actually generated, since those tokens were real provider work.

Detection and prevention

Detection: a spike in the stream_error event rate, segmented by provider and deployment, pinpoints whether disconnects are concentrated on one upstream (pointing at the provider) or spread evenly (pointing at the gateway's own network path). Prevention: this failure mode can't be prevented outright — a mid-stream TCP reset is an external event — so the real prevention target is making sure every streaming code path actually emits stream_error on disconnect rather than silently dropping the connection, which is a testable invariant in itself.

Scenario 6 — Slow client exhausts gateway memory

A mobile client on a degraded connection reads far slower than the provider produces chunks. Without backpressure, the gateway buffers unboundedly per connection; at scale — thousands of concurrent slow readers during a traffic spike — this becomes an OOM risk for the gateway process itself, taking down healthy requests alongside the slow ones.

Fix

Bounded per-connection buffers with upstream read throttling matched to client consumption rate (Architecture Deep Dive, Chapter 09) — the same flow-control discipline as Backpressure generally, applied to SSE relaying specifically.

Detection and prevention

Detection: gateway process memory climbing in correlation with concurrent streaming-connection count (not with request rate) is the specific signature of this failure, distinguishable from a generic memory leak by that correlation. Prevention: load-testing with deliberately slow/throttled readers — not just fast synthetic clients — is the only way to catch an unbounded per-connection buffer before it reaches production, since fast test clients never exercise the backpressure path at all.

Scenario 7 — Billing ambiguity on client cancellation

A user closes a tab 40 seconds into a long stream. The gateway propagates the cancellation and frees the concurrency slot immediately — but the provider may have already generated, and will report as billed, tokens the client never saw. A system that assumes 'canceled = $0 spent' will systematically under-settle its ledger against what providers actually charge.

Fix

Mark the reservation PENDING_RECONCILIATION on cancel rather than immediately releasing it; finalize once the provider's usage report (which may arrive asynchronously after the cancel) confirms the real generated amount.

Detection and prevention

Detection: a growing count of reservations stuck in PENDING_RECONCILIATION past a reasonable provider-reporting SLA (e.g. a few minutes) signals either a provider usage-reporting outage or a bug in the reconciliation consumer — either way, it should page before those reservations silently age out unresolved. Prevention: a scheduled sweep that force-reconciles or escalates any reservation still pending past a hard ceiling, so a provider-side reporting delay can never turn into a permanently stuck ledger entry.

Scenario 8

Idempotency & Retry Pitfalls

A client's own timeout (4.2s) fires before the gateway has heard back from a slower provider call. The client retries with the same idempotency key.

The ledger correctly recognizes the key and deduplicates to one billing entry. But the *original* request may still be executing upstream when the retry arrives — if the gateway's own handling isn't careful, it could initiate a second, independent call to the provider for what the client considers one logical request. The provider then performs — and bills for — two completions, even though the ledger shows only one.

Partial fix, and its honest limit

The gateway can hold a short-lived in-flight marker per idempotency key, so a retry arriving while the original call is still outstanding attaches to the same in-flight request instead of starting a new one (this closes the gap for retries arriving within the same gateway's processing window). What it cannot fix: a retry arriving at a *different* gateway replica after the in-flight marker has expired, or a provider whose own API doesn't expose a way to check an in-flight request's status. See Idempotency for why idempotency keys dedupe your own system's bookkeeping but can't force a remote system's execution semantics.

Detection and prevention

Detection: a per-tenant ratio of 'idempotency keys seen' to 'distinct provider calls made' drifting above 1.0 indicates duplicate upstream execution is actually happening, not just a theoretical risk. Prevention: keeping the in-flight marker's TTL deliberately longer than the gateway's own upstream timeout (so a retry can never outlive the window where it would find no marker) closes the main version of this gap, even though it can't close all of them.

Scenario 9

Guardrail & Security Incidents

A tenant's prompt accidentally includes a live API key pasted from a config file. Separately, another request tries to social-engineer the model into calling a tool the tenant's policy disallows.

The secret-pattern guardrail catches the first case and redacts it before the request reaches a provider or a log line — a clean save. The second case is different: no amount of content-pattern matching reliably distinguishes a legitimate complex instruction from an adversarial one, and a guardrail tuned aggressively enough to try will generate enough false positives to break normal usage.

The actual fix is authorization, not better pattern matching

The tool-call permission check from identity/ACL enforcement (Complete Design, Chapter 05) rejects the disallowed tool call regardless of how the request was phrased — because it's enforced structurally against the tenant's policy, not by inspecting the prompt's intent. Guardrails remain valuable for what they're actually good at (known-pattern secrets, known-category toxic content) and are not relied upon as the access-control boundary.

Detection and prevention

Detection: redaction-triggered events and tool-call authorization rejections are both logged as metadata-only security signals — a spike in either, per tenant, is worth an automated alert regardless of whether the underlying request was ultimately blocked successfully. Prevention: periodic red-team testing of the tool-call authorization boundary specifically (not just the content guardrail) is what catches a policy-enforcement regression, since a guardrail passing its own tests says nothing about whether authorization is still correctly wired to every tool-call code path.

Scenarios 10-11

Control Plane & Policy Failures

Scenario 10 — Control-plane outage during a revocation

An engineer revokes a leaked virtual key at 14:02. The control plane goes down at 14:03 for an unrelated maintenance incident, before the revocation has propagated in a new snapshot. Gateways continue serving on the last snapshot (which still trusts the leaked key) until the control plane recovers and a new snapshot ships — a real, bounded security exposure window, not a hypothetical one.

Fix and its limit

A dedicated, independent low-latency revocation channel (separate from the normal policy-snapshot cadence) for exactly this class of event — confirmed credential compromise — bypasses the control plane's normal distribution path and propagates within seconds via a lightweight broadcast gateways always listen for, even during a broader control-plane outage. This adds operational surface specifically to shrink the one freshness-window case that matters most; it is not used for routine policy changes.

Detection and prevention

Detection: any successful request authenticated with a key flagged as revoked-but-not-yet-propagated is a direct, loggable security event — the gateway can know after the fact exactly which requests fell in the exposure window, from telemetry alone, without needing the prompt. Prevention: paging on 'revocation requested' immediately, independent of whether the normal snapshot or the emergency channel delivers it, so an operator is watching the window close rather than assuming propagation succeeded silently.

Scenario 11 — Pricing change mid-flight

A control-plane snapshot updates a model's price between when a request is admitted (reserved at the old price) and when it finalizes (after the new snapshot has taken effect). If finalization naively uses 'the current price,' a tenant's bill for one request could reflect a price that wasn't in effect when they made the call.

Fix

The reservation record locks the price version at admission time (Architecture Deep Dive, Chapter 04); finalization always uses the locked version, never the live one. The result: during a price change, in-flight requests briefly finalize at the old price while new requests reserve at the new one — a short, intentional, and auditable transition window.

Detection and prevention

Detection: the ledger's per-request price-version field makes this trivially auditable after the fact — a finance reconciliation can always answer 'what price was this specific request billed at and why' without ambiguity. Prevention: price changes are themselves versioned control-plane snapshots, so this isn't a special case to remember — it's the same locked-at-admission mechanism every other policy field already uses, which is the point of routing all policy through one snapshot mechanism instead of a one-off price-update path.

Scenario 12

Region & Residency Failures

A tenant with an EU-residency requirement has its required eu-west-1 deployment go down. A healthy us-east-1 deployment of the same logical model sits right next to it.

Swipe to pan
Residency Beats Convenient FailoverA healthy, wrong-region deployment is not an eligible fallback (Invariant I5)EU-restricted requesteu-west-1unavailableus-east-1healthy — wrong region✗ prohibitedExplicit failureresidency-constrained-unavailable
A healthy, wrong-region deployment is not an eligible fallback

The router's residency filter (Architecture Deep Dive, Chapter 07) excludes the US deployment from the eligible set for this tenant regardless of its health — filtering happens before scoring, and a compliance constraint is exactly the kind of hard constraint that filtering exists to enforce. The request fails with an explicit 503 NoEligibleDeployment, specifically annotated as residency-constrained, rather than silently succeeding against the wrong region.

Detection and prevention

Detection: a residency-tagged 503 NoEligibleDeployment rate greater than zero for a sustained period is a direct signal that a compliance-constrained tenant has no healthy in-region capacity — this should alert operations and account management simultaneously, since it's both a reliability and a customer-relationship event. Prevention: provisioning at least two independently-failing deployments within the compliant region for any tenant with a hard residency requirement, so a single in-region deployment failure doesn't become a total compliance-bounded outage for that tenant.

Scenarios 13-14

Multi-Tenant Scale Failures

Scenario 13 — Noisy neighbor (pre-admission-control baseline)

Covered in depth in Complete Design, Chapter 04 — included here for completeness of the failure catalog. A tenant-blind proxy forwards every caller through a shared credential; one tenant's burst exhausts the shared provider limit and produces 429s for an unrelated tenant's ordinary traffic. Fixed by virtual-key identity plus per-tenant atomic admission (Complete Design, Chapters 05-06).

Scenario 14 — Hot tenant contends for shared infrastructure, even with quotas enforced

After fixing noisy-neighbor with per-tenant quotas, a different, subtler version of the same problem can still occur: a single very high-volume tenant's sheer request rate against the *shared quota store* (not the shared provider) adds latency to the admission check for every other tenant sharing that same Redis cluster or shard, even though each tenant's own budget is correctly isolated. Per-tenant correctness doesn't automatically imply per-tenant performance isolation on shared infrastructure.

Fix

Shard the quota store by tenant-hash across multiple Redis instances/clusters, so one tenant's admission-check volume is physically confined to one shard rather than contending with every tenant on a single shared instance — the same partitioning principle used for hot-key problems in any shared cache.

Detection and prevention

Detection: per-shard p99 admission latency diverging from the fleet-wide average is the earliest signal of a hot shard, well before it's severe enough to show up in overall error rates. Prevention: tracking each tenant's admission-check rate against shard capacity at onboarding time, and re-sharding or giving a dedicated shard to any tenant whose traffic alone would saturate a shared one — treating this as a capacity-planning input, not an incident to react to after the fact.

Scenario 15 — Gateway crash between ledger write and quota settlement, and a timed-out call the provider ran anyway

Trigger: a gateway replica finishes a request, inserts the usage record into the ledger, and is killed before it tells the quota store to settle. Timeline: the ledger shows $0.0822 charged for reservation R-7731, while the quota store still shows R-7731 as RESERVED at the worst-case $0.105. Left alone, the tenant's budget looks $0.105 spent until the reservation expires. A second, related case: an upstream call times out at the gateway, the gateway retries on another deployment, and the first provider had in fact completed the request.

What goes wrong if this is handled naively

A naive expiry job releases every expired reservation in full. For R-7731 that refunds $0.105 on top of a ledger charge of $0.0822, which double-counts money back to the tenant. For the timeout case, releasing the first attempt's reservation would hide a generation the provider billed. A naive retry that charges a second reservation would double-charge the tenant for one logical request.

Fix

Settlement runs ledger insert first, then the quota store's compare-and-set settle script. The reconciler treats an expired RESERVED reservation by checking the ledger: if a usage row exists it runs the same settle script with that usage; if no attempt was ever recorded it releases in full; if an attempt may have executed and no usage is known, it marks the reservation PENDING_RECONCILIATION and waits for the provider's usage report. Retries run under the original reservation, extended only when the quota store can cover the extra possible execution.

Detection and prevention

Detection: the count and age of reservations that are expired but unsettled, and any mismatch between ledger totals and quota-store spend per tenant per day, which a nightly comparison reports. Prevention: every quota-store transition checks the current status before acting, so repeating settle or release is harmless, and the ledger's unique key on reservation ID makes a replayed insert a no-op. Residual risk: a provider that never reports usage for a timed-out call leaves a reservation pending at its worst case until a hard ceiling escalates it to a person, which is a deliberate bias toward over-charging the budget counter, not under-charging it.

Worked Example

Capacity Math for an Incident Review

These are illustrative planning numbers, not measurements of any real incident. Assumptions: 1,200 req/s peak for 5 minutes, 1,500 avg input tokens, 500 max requested output tokens, 10% of requests are long streams averaging 45s, and Provider A's circuit opens for exactly 30s during the peak.

QuantityFormulaResult
Total peak requests1,200 req/s × 300s360,000 requests
Peak reservation ceiling (worst case)1,200 × (1,500 + 500)2,400,000 tokens/s
Requests affected by the 30s Provider A outage1,200 req/s × 30s × (Provider A's traffic share, ~60%)≈ 21,600 requests rerouted to Provider B
Concurrent long streams at steady state (Little's Law)1,200 × 0.10 × 45s5,400 concurrent long-stream slots
Quota-store ops/s at peak (2 checks per request: reserve + finalize)1,200 × 22,400 ops/s against the shared counter store
Ledger writes/s at peak1,200 (one finalize per completed request)1,200 writes/s to the durable ledger

What this shows: the 21,600 rerouted requests during the Provider A blip is a real, bounded number you can hand to Provider B's capacity planning ('can you absorb an extra ~720 req/s for 30 seconds on top of your existing share') rather than an open-ended 'what if everything fails over' question. The 5,400 concurrent long-stream slots is the number that actually sizes gateway memory and connection-pool limits — not the flat 1,200 req/s figure, which undercounts exactly the long-tail traffic that holds resources longest.