中文 English

A 429 Does Not Mean ‘Switch Models’: How One AI Gateway Failure Can Become a Triple Bill

Published: 2026-08-23 · 阅读量 --
AI Agent AI Gateway 模型网关 NewAPI 架构 平台工程 可观测性 工程实践

The conclusion first

Switching models as soon as an upstream returns 429 looks like high availability. It can instead create three failures at once: turn brief congestion into a retry storm, send classified data to an ineligible region, and let several accepted model calls generate one useful answer but several bills. The answer is not to remove fallback. It is to route in the right order: apply hard capability, residency, data-classification, and budget constraints first; rank the eligible routes by health, latency, and cost second; then retry only normalized, recoverable failures inside one bounded budget. A transient rate-limit 429 or brief 5xx may be eligible for fallback. A 401, 403, exhausted balance, or compliance denial must stop and expose the real cause. I built a deterministic, standard-library Python lab with no network and no credentials to prove routing filters, bounded 429 fallback, circuit breaking, hedging cost, and audit budgets. All seven acceptance checks pass.

Original cover: 429 does not mean retry everything; an AI Gateway must route, bound, and prove.

Figure 1: original deterministic PNG cover. Every provider, price, latency, token count, and failure in this article is synthetic teaching data, not a real vendor price or SLA claim.

1. Background: how fallback turns from a safety net into an amplifier

In my production AI Platform reference architecture, the AI Gateway sits in the control plane. It does not merely forward HTTP. It understands model capability, context, data boundaries, cost, and fallback eligibility. Yet a surprising number of implementations reduce that responsibility to this:

try primary model
if error:
    try fallback A
if error:
    try fallback B

The code is short; the incident can be very long. The primary route returns 429, the application retries twice, the SDK retries twice more, the gateway tries two other models, and the outer Agent reruns the complete turn after its deadline. Every layer believes it made one polite recovery attempt. Multiplied together, they behave like several children pressing the elevator button at once: the elevator does not arrive faster, but the controller receives much more work.

Model traffic also is not an ordinary static-file request. One candidate may lack tool calling or strict JSON. Some data may be required to stay in one region. Context limits, input/output pricing, content rules, and streaming event formats vary. Once streaming has begun, swapping models can even splice two incompatible answers into one user-visible message. A route returning HTTP 200 proves that packets travelled; it does not prove that the route was suitable for this request.

2. Symptoms: six production disorders grown from one 429

Bad routing rarely presents as a clean outage. It appears as several confusing symptoms:

  1. Tail latency keeps growing. Every layer retries, so the user waits until the end to see failure. The dashboard shows only the final attempt and hides the time spent before it.
  2. The bill grows faster than successful work. Hedging starts two or three calls and consumes only the fastest answer, while the other upstreams may already be generating tokens.
  3. Quality changes silently. The fallback lacks the same tool, structured-output, or context contract. The HTTP call succeeds, but strict JSON becomes prose.
  4. Safety boundaries are bypassed. The primary route refuses content for a compliance reason; the gateway sends the same data to a route with different rules. That is automated policy bypass, not resilience.
  5. Configuration errors disappear. A 401 becomes “all models are busy.” Operators scale capacity while the invalid credential remains buried in a nested log.
  6. The outage feeds itself. A failing upstream remains overloaded because new requests and retries keep knocking. Eventually fallback traffic overloads the healthy route too.

Think of ride dispatch during a storm. If there is no nearby taxi, expanding the search radius may help. If the passenger’s identity document is invalid, changing taxi companies cannot make it valid. If the destination is inside a restricted zone, a driver must not sneak around the checkpoint. An AI Gateway needs to be dispatcher, triage desk, and ledger—not an automatic phone dialler.

3. Draw the boundary first: AI Gateway is not API Gateway

An API Gateway and an AI Gateway can be adjacent and may share infrastructure, but they answer different questions.

Original comparison: API Gateway and AI Gateway own adjacent but different decisions.

Figure 2: original diagram. An API Gateway is like the entrance to a shopping centre: identity, doorway, and crowd control. An AI Gateway is closer to hospital triage: speciality, severity, available capacity, distance, and price. A production system needs both responsibilities.

An API Gateway typically owns ingress authentication, URL and protocol routing, tenant quota, edge rate limits, WAF controls, certificates, and ingress logs. The AI Gateway must additionally decide:

A self-hosted NewAPI relay is a useful foundation for consolidating models, channels, tokens, and usage. A single entrance, however, does not automatically create a complete routing policy. A production design still needs explicit ownership of the request envelope, normalized errors, total retry budget, circuit breaker, and decision evidence around that entrance.

4. Route correctly: hard eligibility first, weighted preference second

Many routers immediately ask which model is cheapest or fastest. That order is wrong. Capability, residency, compliance, and task type are eligibility constraints. An ineligible route does not enter the race. Price and latency rank only the candidates that remain.

Original routing funnel: capability, residency, and classification filter candidates before health, latency, and price scoring.

Figure 3: original diagram. A hospital does not send a broken arm to dentistry because dentistry is cheaper today. A dispatcher cannot send a car parked in another city merely because it is idle. Hard constraints precede preference scoring.

I recommend creating an immutable, structured envelope before routing. At minimum it contains:

{
  "request_id": "req-demo-001",
  "required_capabilities": ["tools", "json"],
  "allowed_regions": ["region-a"],
  "data_classification": "restricted",
  "input_tokens_estimate": 10000,
  "max_output_tokens": 2000,
  "deadline_ms": 1200,
  "max_cost_usd": 0.05,
  "route_policy": "route-policy-v3"
}

These are synthetic values with no real tenant, machine, address, or credential. The router first excludes missing capability, forbidden residency, insufficient classification allowance, and worst-case cost above the request cap. It then orders eligible candidates by rolling health, P95 latency, queue depth, price, and measured quality.

The local lab puts four synthetic routes in one pool. One is in the wrong region, one lacks tool capability, and one exceeds the worst-case request budget. Only route-fit satisfies every hard constraint.

Real local lab output: the router records every exclusion and selects the only eligible route.

Figure 4: real local stdout capture. The lab makes no network call. The winner is not “the first endpoint that answered”; it is the route left after capability, residency, classification, and cost filtering.

Capability routing: a model name is not a capability contract

Do not encode “model X always supports tools” in business code. Capabilities should be versioned registry data validated by small contract probes. Tool calls, strict JSON, multimodal inputs, context limits, output limits, and streaming event shapes can vary by provider, model revision, and compatibility adapter.

Residency and data class: the shortest route may be illegal

Classify data as public, internal, restricted, or another local scheme. Declare the maximum class and allowed regions for each route. A restricted request must not leave its permitted region because the primary is congested. A compliance refusal must not be washed into success by fallback. If school records belong in the records office, an overcrowded classroom is not permission to store them in a shop across the street.

Cost routing: estimate the worst case, not only the input price

An estimate should cover input tokens, permitted maximum output, cache rules, image or audio charges, and reserved retry capacity. “Price per million input tokens” alone underestimates long output and multimodal work. For an Agent, the meaningful unit is the total cost of one completed business outcome, not the price of one model invocation.

5. A 429 is not one cause: read status and provider sub-code

RFC 6585 defines 429 Too Many Requests and allows a server to supply Retry-After. Model APIs may use the same HTTP status for different business conditions: a brief request-rate limit, an exhausted organisation quota, or insufficient funds. “If 429, switch” destroys that distinction.

Original error decision: transient rate limits and 5xx may fall back; authentication, balance, and compliance failures stop.

Figure 5: original diagram. The routing decision needs HTTP status + provider code + local policy. The same 429 can carry rate_limited, eligible for bounded fallback, or insufficient_quota, which must stop.

Normalize provider-specific responses into a stable internal taxonomy before applying policy:

Normalized cause Typical signal Default action Reason
Temporary rate limit 429 + rate_limited, perhaps Retry-After Bounded backoff or compatible fallback Capacity may recover quickly
Transient upstream failure 500/502/503/504 or connection timeout Bounded fallback; count toward breaker The request itself may be valid
Authentication failure 401 or invalid_auth Stop and alert Switching hides a credential failure
Permission failure 403 or permission_denied Stop A fallback must not bypass authorization
Balance or organisation quota exhausted insufficient_quota, balance_exhausted Stop This is a budget fact, not congestion
Compliance or residency denial compliance_denied and similar Stop and audit Fallback may become policy bypass
Invalid request or context 400, schema/context error Repair request; do not retry The same bad request stays bad elsewhere

“Stop” means no automatic provider fallback; it does not mean swallowing the error. The gateway should return a specific, redacted, actionable category and trigger control-plane repair. If an organisation truly wants to move spending after a provider balance is exhausted, that should be a pre-approved policy with an explicit total budget—not a generic 429 retry disguised as resilience.

6. Bounded retry: four ledgers, not merely “retry twice”

max_retries=2 by itself is not enough. A reliable request needs four budgets that constrain one another:

  1. Attempt budget. How many tries are allowed across the complete stack—not separately in every layer.
  2. Time budget. Attempts, backoff, and fallback all consume the same deadline.
  3. Token and money budget. Work produced before failure may be billable; reserve the next route’s worst case before calling it.
  4. Concurrency budget. Bound concurrent work per tenant, task, and upstream so hedging and retries cannot multiply freely.

Backoff needs jitter so thousands of clients do not wake at the same boundary. Retry-After is useful but remains subordinate to the local deadline and budget. If an upstream asks for 30 seconds while an interactive request has 800 milliseconds left, the right result is a quick explicit failure or an eligible fallback—not a request that vanishes into the background.

In the lab, the primary returns synthetic 429 rate_limited after 80ms. The gateway applies a deterministic 100ms demonstration backoff; a compatible fallback succeeds in 450ms. Total time is 630ms inside a 1200ms deadline, and the unified attempt budget blocks a third call.

Real local lab output: rate-limit 429 uses one backoff and compatible fallback; the budget blocks a third attempt.

Figure 6: real local stdout capture. Attempts, aggregate cost, and remaining time live in one evidence chain. outbound_network_calls=0 confirms every provider is a local synthetic object.

The opposite experiment injects 401, a balance-exhaustion 429, and a compliance 403. All three normalize to STOP, and the fallback call count remains zero.

Real local lab output: 401, exhausted-balance 429, and compliance 403 all stop without fallback.

Figure 7: real local stdout capture. Availability must not hide identity, budget, or compliance root causes. Those failures enter alerting and repair instead.

7. Circuit breaker: protect the route, not only one request

A retry budget asks, “How many more attempts may this request make?” A circuit breaker asks, “Should everyone stop knocking on this broken door for a while?” Without a breaker, thousands of requests each perform their own polite retries and convert a local fault into platform-wide queuing.

Original circuit states: CLOSED, OPEN, and HALF_OPEN transition through failure threshold, cooldown, and recovery probe.

Figure 8: original diagram. When a household circuit shorts, the breaker opens. It does not allow every appliance to try ten more times. After cooldown, one small probe determines whether full traffic is safe.

A production breaker needs careful boundaries:

The local experiment makes one route return 503 three times. The third failure opens the breaker; request four fast-fails with zero upstream calls. Before cooldown expires it remains OPEN. At the boundary it admits one HALF_OPEN probe and returns to CLOSED only after that probe succeeds.

Real local lab output: repeated 503 opens the circuit, cooldown fast-fails, and a successful half-open probe restores service.

Figure 9: real local stdout capture. A virtual clock makes transitions deterministic and removes real waiting, which is ideal for a CI release gate.

8. Hedging: lower tail latency, or three invoices for one answer

Hedging is not retry-after-failure. It starts an identical call after the first has remained incomplete for a delay and adopts the earliest valid result. That can help idempotent, read-only calls with expensive tail latency. It can also hide the largest waste behind a better average.

The lab deliberately models a pessimistic case. Route A accepts at 0ms, B at 200ms, and C at 400ms. B completes first at 700ms, so the gateway uses B and cancels A and C. All three have already accepted work. At a synthetic $0.036 each, total cost moves from $0.036 to $0.108—exactly 3x.

Real local lab output: three hedged routes accept work, one answer is used, and synthetic worst-case cost reaches 3x.

Figure 10: real local stdout capture. This is a synthetic worst case designed to test the budget guard. It does not claim that every provider bills cancellation identically. By the time cancellation arrives, an upstream may have done no work, partial work, or all work; production accounting must use the provider’s contract and usage receipt.

If hedging is justified, I impose these gates: inference only, with no side effect; at most two routes by default; a dynamic delay based on historical P95/P99; worst-case cost reserved before the second call; immediate cancellation after the first valid answer; separate loser token, time, and charge metrics; never duplicate a tool execution. Streaming is even stricter. Once text is visible to the user, the gateway cannot silently splice another model into the unfinished answer.

9. Budget is not a dashboard: it must stop work before the call

Many platforms have a cost dashboard that displays the curve after the invoice exists. That is a rear-view mirror, not a brake. Effective control uses reserve, then settle. Reserve from estimated input, maximum output, and the price table; settle with actual usage; release the difference. Recompute aggregate worst-case cost before every retry or fallback.

Budgets should exist at four levels: model attempt, complete request, complete Agent task, and tenant/team time window. The first two stop a sudden outlier; the latter two stop slow chronic loss. Exhausted budget should produce BUDGET_EXCEEDED, not masquerade as model congestion.

Original audit receipt: policy, candidates, exclusions, attempts, and cost are visible while credentials and raw prompt stay out.

Figure 11: original diagram. A route receipt is like a taxi receipt: why this car was selected, which path it used, and what it cost. It does not need the passenger’s card PIN or full private conversation.

In the lab, the primary has already accumulated a synthetic $0.020 partial charge before returning 503. The fallback worst-case estimate is $0.042. Projected aggregate cost is $0.062, above the $0.050 request cap, so the gateway rejects fallback before calling it.

Real local lab output: projected aggregate cost exceeds the request cap, so the premium fallback is never called.

Figure 12: real local stdout capture. The audit retains request ID, policy version, candidates, exclusions, attempts, and synthetic cost. Credentials are redacted, and the raw prompt is not part of the route log.

This is why my article on AI’s hidden bill focuses on cost per successful outcome. Lower unit price does not guarantee lower total cost. When context size is the dominant cost, Headroom in front of NewAPI may help, but compression savings and retry or hedging waste still belong in the same task ledger.

10. Observability and audit: answer “why this route?”

An explainable route decision should record at least:

Do not log full prompts, responses, API keys, or authorization headers by default. Most investigations need structured metadata, content hashes, carefully sampled redacted excerpts, and controlled access to evidence. OpenTelemetry’s GenAI semantic conventions are a useful starting point for spans and metrics, but they continue to evolve. Pin a version and define a low-cardinality local attribute policy.

The dashboard I want includes fallback rate, terminal-stop rate, route P50/P95/P99, circuit-open duration, half-open result, hedge winner and loser tokens, usage after cancellation, cost per successful task, budget denials, and the number of ineligible routes correctly filtered. A correct safety refusal is a successful control outcome, not an availability failure.

11. Zero-network fault lab: what seven PASS results prove

The downloadable standard-library Python lab imports no vendor SDK, reads no credential, and calls no real model. It replaces socket.socket and socket.create_connection with a fail-closed guard, so an accidentally added outbound connection fails immediately. The current run reports zero outbound attempts.

It proves six mechanisms and one global safety condition:

  1. capability, residency, classification, and budget eligibility filtering;
  2. rate-limit 429 fallback inside attempt, time, and cost budgets;
  3. no fallback for 401, 403, exhausted balance, or compliance refusal;
  4. repeated 503 opens a breaker and one half-open probe restores it;
  5. hedging’s unused routes remain visible as cost risk;
  6. aggregate worst-case cost blocks fallback before a call;
  7. the complete lab uses no real credential and no outbound network.

Real local lab output: all seven AI Gateway fault-lab acceptance checks pass.

Figure 13: real local stdout capture. Seven PASS results prove that the control logic behaves as designed. They do not prove a real provider’s quality, price, throughput, or SLA.

Structured output is available as events.jsonl and summary.json. Every provider name, price, latency, token count, and response is synthetic. The dataset is suitable for teaching, regression tests, and CI—not a provider leaderboard.

12. One-click runs on Windows 11, Ubuntu 26.04, and macOS 26

The full instructions are in the README. Each wrapper needs only Python 3 on the machine. No Docker, database, or third-party service is required.

Windows 11

Download the PowerShell wrapper with the lab files and run:

powershell -ExecutionPolicy Bypass -File .\run-windows11.ps1

The wrapper prefers py -3, then tries python. It parses summary.json and succeeds only when all seven checks pass and outbound attempts equal zero.

Ubuntu 26.04

bash run-ubuntu-2604.sh

The Ubuntu wrapper uses set -euo pipefail and re-parses the acceptance file with the standard library.

macOS 26

bash run-macos-26.sh

The macOS wrapper has identical acceptance semantics so results are easy to compare. I executed it in the local macOS Python environment while producing this article.

Manual scenario execution

To inspect scenarios one by one:

python3 ai_gateway_fault_lab.py --scenario routing
python3 ai_gateway_fault_lab.py --scenario bounded-429
python3 ai_gateway_fault_lab.py --scenario terminal-errors
python3 ai_gateway_fault_lab.py --scenario circuit
python3 ai_gateway_fault_lab.py --scenario hedging
python3 ai_gateway_fault_lab.py --scenario budget-audit

On Windows, replace python3 with py -3. “One-click” here means one safe local simulation. The scripts do not discover, modify, or call a production gateway.

13. Agent-driven configuration: automate execution, not weaker acceptance

I also include a downloadable AGENT_TASK.md. Give the directory to a local Agent with this task:

Work only inside the current AI Gateway fault-lab directory.
1. Read README, AGENT_TASK.md, and the Python lab.
2. Select exactly one wrapper for Windows 11, Ubuntu 26.04, or macOS 26.
3. Add no real endpoint, machine name, API key, or network access.
4. Run the wrapper and parse lab-output/summary.json.
5. Report success only if acceptance.passed=7 and outbound_attempts=0.
6. Report every failed check and stderr. Never “fix” a run by deleting an
   assertion, enlarging a budget, or skipping a scenario.

The Agent gets no broader authority than a human operator. It selects the OS wrapper, parses structured output, and prepares the report. Automation must not be free to edit the acceptance standard. Moving a policy into a real gateway should be a separate controlled change: export and redact the current state, generate a diff, obtain approval, run shadow or canary traffic, and preserve rollback. This zero-network lab deliberately does not touch production.

14. Pre-release fault injection: the evidence I put in the gate

Before a real AI Gateway release, I would inject at least:

The acceptance target is not “eventually returned some words.” It is: bounded total retries, bounded parallelism, no data-boundary violation, visible terminal causes, a breaker that stops pressure, attributable cost, and an explainable choice.

15. Mapping the design onto NewAPI, Headroom, and an AI Platform

If NewAPI is already deployed, it can continue to own the unified entrance, channels, tokens, mappings, quota, and usage. Add the request envelope, normalized taxonomy, end-to-end retry budget, breaker, and decision audit in or around the layer that has the required context. Do not independently enable broad retries in the client SDK, Agent framework, and NewAPI. Choose one owner that sees the global deadline and aggregate cost; disable or sharply narrow the others.

Headroom solves long-context compression, not error classification or circuit breaking. It is the vacuum-packing table at the warehouse entrance; the AI Gateway is dispatch. Re-estimating tokens after compression can improve the budget, but Anthropic-native traffic, OpenAI-compatible traffic, and tool contracts must not enter one fallback pool without contract tests. The Headroom deployment article covers that protocol boundary in detail.

In a fuller AI Platform, Turn Manager owns the deadline and cancellation tree; AI Gateway owns model routing, error classification, and cost; Policy Engine owns data and compliance eligibility; Worker owns tool side effects. A model fallback must never duplicate Worker execution. That is why “retry inference” and “retry business action” are distinct policies. The AI Platform architecture reading map provides the broader series context.

16. Q&A

Q1: Can every 429 switch models?

No. Read the provider sub-code, Retry-After, remaining request budget, and local policy. A temporary rate_limited condition may use bounded backoff or fallback. Exhausted funds or organisation quota should stop by default. HTTP 429 alone is not enough information.

Q2: Why not route a 401 to another provider with independent credentials?

Automatic switching lets credential-rotation faults survive unnoticed and may move data away from the identity path intended for it. Stop and repair is the auditable default. If independent credential-pool disaster recovery is a real requirement, encode it as an explicit approved route policy with data and budget constraints—not a universal if error branch.

Q3: How many retries are correct?

There is no universal count. It depends on total deadline, arrival rate, upstream capacity, expected failure duration, and cost. Interactive requests usually benefit from fewer, faster attempts; offline work may wait longer but still needs a shared total budget. More important than the number: one layer owns retry, uses exponential backoff with jitter, and records every attempt in one trace.

Q4: Does OPEN mean every request fails?

It means calls to that route fail fast. An eligible fallback may be used inside the remaining total budget. If no candidate passes hard constraints, explicit failure is more correct than using an incompatible or non-compliant model.

Q5: When is hedging worthwhile?

When the call is read-only and idempotent, tail latency is materially expensive, output is short, route contracts and quality are close, and the budget can pay for duplicates. Never hedge tool side effects. Do not deploy it without loser-usage metrics, and do not make three-way parallelism the default.

Q6: Does cancelling the loser guarantee no charge?

No. Cancellation is best effort. The upstream may have started or completed work before receiving it, and providers differ in accounting. Reserve worst-case cost, settle against real usage receipts, and measure waste after cancellation.

Q7: Must the AI Gateway be a separate microservice?

No. It can begin as a well-owned module in a modular monolith. What matters is one error taxonomy, one aggregate budget, and one decision record—not process count. Split it when scaling, privilege, team ownership, or failure domains truly differ.

Q8: Are NewAPI retries and channel priorities sufficient?

They may be sufficient for a small personal deployment. Restricted data, cross-region rules, multiple tool protocols, and strict budgets require verifying the exact deployed version’s taxonomy, route granularity, breaker, total deadline, and audit evidence. Test with injected faults rather than guessing from a product label.

Q9: Why does the lab avoid real models?

It tests control mechanics. A real API introduces network variance, credentials, changing prices, and non-repeatable latency, making state-machine tests weaker. First prove deterministic boundaries offline; then add provider contract tests and canaries in an isolated environment. The evidence is complementary.

Q10: Which three controls should land first?

A normalized error taxonomy, one end-to-end retry/deadline budget, and an explainable route-decision record. They answer “may we retry?”, “what remains?”, and “why this path?” Add circuit breaking, hedging, and advanced scoring on top of that foundation.

17. References and the final rule

This is not an argument against fallback. I want fallback to remain a safety net instead of becoming a blanket over the root cause. The final rule is simple: decide whether a route is eligible before deciding whether it is attractive; retry only recoverable failure and only within total time and total cost; when identity, funds, or compliance says stop, no backup model is allowed to say continue.

本文阅读量 --