AI Gateway System Design: From Proxy to Production
An AI Gateway looks like an HTTP reverse proxy at first glance — many callers, one or more backends. It isn't. What it actually protects is identity, money, and the semantic correctness of a streamed, non-deterministic response. This module builds one step by step: direct SDK calls, a noisy-neighbor incident, virtual keys and quotas, model-aware routing, streaming failure semantics, budget reservation, and a real distributed control/data-plane platform.
Problem Definition
Acme Support needs an LLM answer today — one team, one provider, one API key. Eleven other teams inside the same company are about to want the exact same thing. The question is how you give all twelve teams access to one or more model providers without any team's traffic, budget, or safety posture bleeding into another's.
This is not primarily a networking problem, even though it looks like one. A generic reverse proxy already solves 'route requests from many callers to a backend.' What it doesn't solve is: who is calling, what are they allowed to use, how much can they spend before anyone notices, what happens when a response is half-streamed and the provider disconnects, and how do you add a second provider without rewriting every client.
Strip away implementation detail and an AI Gateway has three jobs: govern access (identity, model permissions, atomic admission against shared budgets), route reliable inference (pick a healthy, compatible, compliant deployment and translate honestly to its native shape), and account for usage (reserve before committing, settle to the real number afterward, trace without needing the prompt text). Nearly every design mistake in this space comes from solving one of these three and assuming the others come for free.
Visual Design Journey
Watch an AI Gateway evolve from a single direct SDK call into a real multi-tenant platform — virtual keys, atomic token/budget reservation, model-aware routing, circuit breakers, streaming failure semantics, and a control/data-plane split — each piece earned by a concrete incident, not added by assumption.
Requirements
Scope this the way you would in an interview — explicitly, before any diagram — because the scope decisions themselves are half the design.
Functional (V1)
Chat and text-generation completions, including server-sent-event streaming. Backend/service clients only — no browser ever holds a provider credential. Tenant and model access control from day one. Two upstream provider deployments to start, reachable through one or more logical model names. Usage metering and a spend budget per tenant. Embeddings, image generation, and autonomous multi-step agents are explicitly out of scope for V1 — they have different latency, payload, and safety shapes and deserve their own design.
Non-functional
Tenant isolation
One tenant's traffic, quota exhaustion, or misconfiguration must never degrade another tenant's service. This is the #1 correctness invariant for this design — the equivalent of 'no double-selling' in a booking system.
Bounded financial exposure
Because output length isn't known until generation finishes, the gateway can never promise zero possible overshoot against a budget — only a documented, bounded worst case, enforced by an atomic reservation made before admission.
Streaming honesty
Once a response has started streaming to a client, a retry or fallback can never silently replace what's already been shown with content from a different generation. Partial failure must be surfaced explicitly, never papered over.
Provider-format honesty
A shared request/response shape across providers is a convenience, not a guarantee that every provider supports every feature. An unsupported capability is rejected explicitly, never silently dropped.
Low admission overhead
Authentication, authorization, and quota/budget reservation sit on every single request's critical path — they need to add single-digit milliseconds of overhead, not a noticeable fraction of total latency, which is comfortably true for an atomic cache operation and comfortably false for a synchronous call into a slow control plane.
Prompt privacy by default
Operational observability (tenant, model, tokens, cost, latency, routing decision, finish reason) must be fully traceable without ever requiring the prompt or completion text. Payload capture is opt-in, purpose-limited, and redacted.
Illustrative scale
For reasoning purposes only: 120 req/s average, 1,200 req/s peak for ~5 minutes, ~1,500 average input tokens, up to 500 requested output tokens, ~10% of requests are long streams. These are planning numbers, not measured production traffic — treat every number in this module the same way.
V0 — Direct SDK Integration
Start with the simplest thing that could possibly work: Acme Support's backend holds a provider API key directly and calls the provider's chat completion endpoint.
This is not a strawman. For exactly one team calling exactly one provider, with the key held server-side and never shipped to a browser, V0 is a completely legitimate production architecture. A gateway earns its complexity later, by solving demonstrated problems — not by assumption.
The cracks appear the moment a second team needs the same capability. Each new team writes its own HTTP client, its own retry and timeout logic, its own parsing for that provider's specific error shapes. One team handles 429s gracefully; another silently returns a blank response to its users. Nobody can answer 'how much are we spending on this provider today' without a shared place to look. And twelve independently-held API keys scattered across twelve codebases is a security and operations problem on its own — rotating one key means coordinating twelve deploys.
The Reverse Proxy and the Noisy Neighbor
Put one stateless HTTP proxy in the middle. Every team calls one URL instead of calling the provider directly. Integration duplication mostly disappears — but a new failure mode appears almost immediately.
A generic reverse proxy (API Gateway fundamentals covers this layer in general) solves transport-level duplication: one place for retries, one place to see traffic. What it does not solve is that the proxy has no concept of who is calling. It forwards every request with the same shared provider credential, to the same upstream, regardless of source.
Nine in the morning: Team B kicks off an overnight reconciliation batch — thousands of requests, through the shared proxy, to the shared provider deployment. The provider's own rate limit is shared across every caller using that credential, so Team B's batch alone exhausts it. Team A sends three ordinary requests a few minutes later. All three come back 429 — not because Team A did anything wrong, but because somebody else used up the shared budget first.
Identity: Virtual Keys & Model ACLs
Fix the proxy's blind spot by giving every caller its own identity. This single change is what makes every governance decision after it possible.
Virtual keys
Each tenant gets a key the gateway itself issues and controls — not the provider's real key. The gateway maps that virtual key to a tenant, a team, and a policy. The real provider credential is injected server-side, immediately before the call upstream, and never returned to a client or logged in plaintext.
Model access control lists (ACLs)
Identity alone isn't enough — the gateway also needs to know what each tenant may use. Team A's policy might allow a fast, cheap model class but not an expensive reasoning model. A disallowed model request is rejected with 403 *before* any network call to a provider — no tokens spent, no upstream latency, no leaked capacity.
401 vs. 403 — a real distinction
A request with no valid credential at all gets 401 (unauthenticated — 'I don't know who you are'). A request with a valid credential naming a disallowed model gets 403 (unauthorized — 'I know exactly who you are, and the answer is no'). A well-built client treats these very differently: 401 might mean 'refresh my credential,' 403 should never be blindly retried.
Atomic Admission: Quota & Budget Reservation
Identity tells the gateway who is asking. It doesn't yet protect a tenant's fair share of a shared resource across multiple gateway replicas — that needs atomic admission.
Run two stateless gateway replicas behind a load balancer, each checking its own local, in-memory counter for 'tokens remaining this minute.' Say one unit of budget is left. Both replicas see '1 remaining' locally. Both admit a request. The real shared limit is exceeded — not because either replica made a mistake, but because *local* and *shared* are different things, and nothing coordinated between them. This is the same read-then-write race as any double-booking bug.
Shared atomic counter
Replace local counters with a shared, atomic store (a Redis-backed token bucket is the common choice) that every replica calls before admitting a request. The check-and-decrement happens as one indivisible operation, so only one of two simultaneous requests for the last unit can win; the other is denied with honest retry guidance (the provider's real Retry-After, not an invented number).
RPM and TPM are separate constraints
A request-count budget and a token budget don't move together — one large request can exhaust token budget while barely touching request-count budget. Both need their own atomic reservation, checked independently.
Reserve the worst case, settle the real number
At admission time, output length is unknown — only the client's requested cap is known. Reserve pessimistically: estimated input tokens (using the right tokenizer for this model) plus the full requested output cap. When the request finishes using less, the unused part is refunded to the budget at settlement. The number shown mid-request is a reservation, not a bill.
Budget reservation is the same pattern, for money
The identical race exists for spend: forty concurrent requests, each individually fine against a daily cap, can collectively blow past it if spend is only checked *after* each request completes. The fix is the same shape — reserve a conservative dollar estimate atomically at admission, record the real amount in the ledger on completion and refund the rest of the reservation. This bounds exposure; it does not make overshoot impossible, because tokenizer differences and delayed provider usage reporting leave a small, honestly-documented gap.
Model-Aware Routing
A client naming a provider's exact model directly is fragile — deprecate that model, or add a second provider for the same job, and every caller's code has to change. Introduce a logical model name instead.
Logical models
Clients only ever ask for a stable name like 'support-fast.' The gateway's model registry maps that name to one or more approved deployments, each with documented capabilities: streaming, tool calls, context length, JSON mode. This is a stable contract for the client and a flexible routing decision for the gateway.
Adapters translate honestly
Each provider deployment has its own adapter that translates the gateway's standardized request into that provider's native shape. When a target deployment can't honor a requested capability — say, structured JSON output — the adapter rejects the request explicitly rather than silently dropping the requirement. A caller that thinks it got structured output but actually got free text can crash downstream, or worse, silently misparse.
Filter first, score second
The router eliminates any candidate that fails a hard constraint — required capability, data residency, policy — *before* comparing price or latency. A cheaper, disqualified deployment never enters scoring. Getting this ordering backwards is how compliance incidents happen: scoring first and discovering the disqualification afterward.
Bounded fallback before commitment
If the first-choice provider rejects a request (429, 5xx) *before any bytes have been sent to the client*, a bounded retry against a capability-compatible, healthy alternative is safe. The client never sees the first provider's bad moment. This safety condition disappears the instant a response starts streaming — see Chapter 08.
Circuit breakers stop hammering a failing deployment
Six consecutive failures from a deployment opens its circuit — new requests skip it entirely for a cooldown window, favoring a healthy alternative. After cooldown, a single half-open probe decides whether to close the breaker (deployment is trusted again) or restart the cooldown. See Circuit Breaker for the general pattern this specializes.
Streaming Changes the Rules
Most of this traffic streams. Streaming is not a transport detail bolted onto the same failure model as a normal request — it changes what failure and retry even mean.
TTFT vs. total completion time
Time-to-first-token and total completion time fail independently — a system can have excellent TTFT and still take ten seconds to finish. Track both; a single flat latency number stops meaning much once ~10% of requests are long streams.
Backpressure on slow readers
If a client reads slowly (a flaky connection, a backgrounded tab) and the gateway keeps buffering provider chunks without limit, enough concurrent slow clients becomes an unbounded memory problem. Apply the same flow control any producer-faster-than-consumer system needs — see Backpressure — slow reading from upstream, or cap the buffer and drop the connection past a limit.
Mid-stream failure is not retryable the same way
Seventeen chunks into a response, the provider connection drops. The client has already rendered half an answer — there's no safe way to seamlessly continue that exact sentence on a different model or a different generation; claiming it's the same completion would be a lie. The gateway terminates the stream with an explicit error event and marks the response incomplete. It never pretends success.
Client cancellation isn't automatically a refund
If a caller disconnects mid-stream, the gateway propagates the cancellation upstream where supported and frees the concurrency slot immediately — but the provider may already have generated (and billed) tokens the client never received. The reservation stays charged until the provider's usage report confirms what was actually generated, and only then is the unused part refunded.
Spending, Safety & Observability
Three more properties earn their place before this is a real platform: idempotency under retry, content safety that doesn't substitute for authorization, and tracing that never needs the prompt.
Idempotency has a real limit
A client that times out and retries with the same idempotency key gets its ledger entry deduplicated to one — but the original request may still be executing upstream when the retry goes out, so the *provider* can end up running inference twice for what the client experiences as one logical request. A local ledger (Idempotency) dedupes your own bookkeeping; it cannot force a remote provider to have exactly-once execution semantics. State that gap honestly rather than promising it away.
Guardrails filter content; they don't replace authorization
A guardrail that detects and redacts a pasted secret before it reaches a provider or a log line is useful, and imperfect — false positives and false negatives are both normal. But a prompt that tries to talk its way into an disallowed tool call is an authorization problem, not a content-filtering one. No amount of pattern matching on text substitutes for the tenant's actual model/tool permissions enforced back in Chapter 05.
Observability without payloads
A single correlation ID follows a request through every hop — client, gateway, chosen deployment, back — carrying tenant, logical model, deployment chosen and why, TTFT, estimated/actual tokens, estimated/actual cost, any policy action taken, and the finish reason. None of that requires the prompt body; payload capture stays off by default.
Scaling to a Real Platform
Twelve hundred requests a second, with long-running streams in the mix, needs more than one gateway process — and needs policy to change without touching the traffic path.
Stateless data-plane replicas
Spread traffic across multiple gateway replicas behind a load balancer; all of them share the same atomic quota store and the same durable ledger, which is what makes 'stateless' safe — any replica can handle any request because none hold state the others lack. Replicas scale request-handling throughput and failure isolation; they do not increase the upstream provider's own rate limit, which remains the real ceiling.
Control plane / data plane separation
Tenant policy, the model registry, pricing, and provider credentials change far less often than requests arrive, so they live in their own control plane. Changes produce a new versioned policy snapshot, pushed asynchronously to every gateway replica — never a live admin-database query inside a per-request hot path, which would put the slowest, flakiest part of the infrastructure on the critical path of every chat response.
Control-plane outages are survivable, with an honest caveat
Because gateways hold the last-known-good snapshot, they keep serving normally during a control-plane outage. The documented cost: a permission revoked moments before the outage might not take effect until the next snapshot arrives, bounded by the policy's freshness window. A design that insists on instant revocation would need a synchronous control-plane call on every request — exactly the hot-path dependency just avoided.
Compliance beats convenient failover
If an EU-restricted tenant's required deployment goes down, a healthy US deployment of the same logical model is *not* an eligible fallback — compliance constraints filter the candidate set before the router ever scores anything, exactly as in normal-path routing, just now under failure.
Final Architecture
Every component below exists because of a specific, demonstrated failure earlier in this story. None of it is decoration — and a small, single-provider deployment may not need every piece at once.
- 01Gateway (stateless ×N): authenticates virtual keys, enforces model ACLs, orchestrates admission and finalization
- 02Quota Store: shared, atomic RPM/TPM reservation — the admission hot-path coordination point
- 03Router: capability-aware filtering (residency, capability, policy) then scoring (price, latency, health)
- 04Model Registry: logical-model → approved-deployment mapping with documented capabilities
- 05Adapters: per-provider request/response translation with explicit capability-mismatch rejection
- 06Providers: upstream model deployments, tracked individually for health and circuit-breaker state
- 07Ledger: durable, idempotent usage/budget settlement — authoritative, not the hot-path check
- 08Control Plane: policy, pricing, and credential distribution via versioned async snapshots
- 09Telemetry: correlation-ID tracing across every hop, metadata-only by default