Design an LLM gateway with failover and budgets
The interviewer asks
“Your company has 40 product teams calling OpenAI, Anthropic, Gemini and a self-hosted model directly from their services. Costs are out of control and every provider outage becomes a product outage. Design one internal gateway that every LLM call goes through, with routing, failover, caching and per-team budgets.”
What a strong answer looks like
A strong answer treats the gateway as a thin, very reliable proxy on the hot path, not a smart application. Start with the request pipeline: authenticate a virtual key, resolve the route for (team, feature), check the budget, try the exact cache, then call the provider with timeouts, retries and a circuit breaker, streaming tokens straight through. Everything else (usage events, cost, prompt logs) happens asynchronously. The hard parts are: enforcing budgets when the cost is only known after the response (reserve, then settle), failing over safely (only before the first token, to a model that passed evals for that feature), spreading load across provider quotas (tokens per minute per key and region), and keeping the gateway itself from becoming the single point of failure. Senior candidates add data-residency routing, prompt-cache-friendly ordering, cancellation on client disconnect, and a clear story for what the gateway does not do (prompt logic, agents).
Concepts to mention
1 Requirements
Functional
- One OpenAI-compatible API (chat, embeddings) in front of several providers and self-hosted models
- Route by feature: each (team, feature) has a primary model, ordered fallbacks, max tokens and timeout
- Automatic failover on 429, 5xx and timeouts, with a circuit breaker per provider deployment
- Per-team and per-feature budgets in tokens and rupees/dollars (daily and monthly), with soft and hard limits
- Streaming passthrough (SSE) with time-to-first-token unchanged by the gateway
- Exact-match response cache for deterministic calls; optional semantic cache per feature
- Usage, cost and latency recorded per call and per tenant; prompt logs with PII redaction and retention
- Virtual API keys per team so real provider keys never leave the gateway
Non-functional
- Gateway overhead under 15 ms p99 (excluding the model call)
- 99.99% availability for the gateway itself; must survive a full outage of any one provider
- Peak 1,500 requests/s and ~10,000 concurrent open streams
- Budget overshoot bounded to under 2% of the limit
- Usage data visible on dashboards within 1 minute
- Prompts of tenants with India-residency requirements never leave in-region endpoints
Out of scope (say it out loud)
- Prompt templates, agents and RAG logic (owned by product teams)
- Training or fine-tuning models
- Customer-facing billing invoices (the gateway only emits usage events)
- Running the GPU inference servers themselves
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/chat/completions | OpenAI-compatible chat call. Headers: Authorization (virtual key), X-Feature, X-Tenant-Id, Idempotency-Key. stream=true returns SSE. |
POST/v1/embeddings | Embedding calls, routed and metered the same way. |
GET/v1/budgets/{team} | Current spend, remaining budget, reset time per feature. |
PUT/admin/v1/routes/{team}/{feature} | Set primary model, fallback chain, max_tokens, timeouts, cache policy, residency. Versioned and audited. |
PUT/admin/v1/budgets/{team} | Set daily/monthly limits, soft-limit action (alert, downgrade model) and hard-limit action (429). |
POST/admin/v1/keys | Issue or rotate a virtual key scoped to a team, features and models. |
4 Data model
routesPostgres → pushed to gateway memoryteamfeature (PK pair)primary_modelfallbacks[]max_tokensttft_timeout_mstotal_timeout_mscache_policyresidencyversionLow write rate, needs audit. Gateways load the whole table at start and refresh on a change notification (Postgres LISTEN or etcd watch), so no DB call on the hot path.
provider_deploymentsPostgres + memorydeployment_id (PK)providermodelregionapi_key_ref (secret manager)tpm_limitrpm_limitprice_versionOne model can have many deployments (Azure regions, several org keys). The router load-balances across them by remaining quota.
virtual_keysPostgres + local cachekey_hash (PK)teamallowed_features[]allowed_models[]statuscreated_atOnly the hash is stored. Cached in gateway memory for 60 s so revocation takes effect within a minute.
budget_countersRedis Clusterbudget:{team}:{feature}:{day} → spent_micro_usdreserved_micro_usd; TTL 35 daysAtomic Lua for reserve/settle. Hash tag on team keeps one team's keys on one shard.
exact_cacheRediskey = sha256(model, params, system, messages, tools) → responseusage; TTL per featureOnly for temperature 0 or features that allow reuse. Scoped by tenant inside the hash.
usage_eventsKafka → ClickHouserequest_idtsteamfeaturetenantmodeldeploymentinput/output/cached tokenscost_micro_usdttft_mstotal_msstatuscache_hitfailover_fromColumnar store for cost dashboards and anomaly alerts; partition by day, order by (team, feature, ts).
price_tablePostgresmodelprice_versioninput_per_moutput_per_mcached_input_per_mvalid_fromVersioned so old calls are never re-priced with today's price.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
A streamed chat call
- 1The support-summary service sends POST /v1/chat/completions with its virtual key, X-Feature: ticket-summary and stream=true.
- 2The gateway checks the key hash in its local cache and loads the route for (support, ticket-summary) from memory: primary model A, fallback B, max_tokens 500, TTFT timeout 4 s.
- 3It counts input tokens with the model's tokenizer and reserves the worst-case cost (input + max_tokens) in Redis with one Lua call; if the hard limit would be exceeded it returns 429 with the reset time.
- 4It checks the exact cache (temperature 0, cacheable feature). Miss.
- 5The router picks the deployment of model A with the most remaining tokens-per-minute quota whose circuit is closed, and opens a streaming request.
- 6The first token arrives in 600 ms; the gateway flushes each SSE chunk to the caller as it arrives, never buffering the whole answer.
- 7At the end the provider's usage block gives exact tokens (including cached input). The gateway settles the Redis reservation to the real cost.
- 8Asynchronously it publishes a usage event to Kafka and a redacted prompt/response record to S3; the response goes into the exact cache.
Primary provider outage
- 1Provider A starts returning 503 and timeouts in one region.
- 2Each gateway instance tracks errors per deployment in a sliding window; after 50% errors over 20 requests the breaker opens for 30 s.
- 3New requests skip the open deployment and try A's other region; if all A deployments are open, the route's fallback B is used.
- 4A request that failed before any token was sent is retried once on the next target inside the same deadline; the caller only sees extra latency.
- 5Usage events carry failover_from=A, so dashboards show the switch; after 30 s a few half-open probes test A and close the breaker when they succeed.
Team hits its budget
- 1The search team's daily budget is ₹4 lakh with a soft limit at 80%.
- 2At 80% the aggregator alerts the team on Slack and, if configured, the route switches to the downgrade model for low-priority features.
- 3At 100% the gateway's reserve call fails and returns 429 with a Retry-After at budget reset and a clear error code budget_exhausted.
- 4The team's owner can raise the limit in the admin API (audited), which takes effect on the next request.
7 Deep dives
Budgets when cost is known only after the call
You cannot know the output tokens before the model answers, and a streaming call can run for 30 s. Checking 'spent < limit' and adding the cost afterwards lets 2,000 concurrent requests all pass the check at 99% and overshoot badly. Use reserve-then-settle. Before the call, count input tokens exactly with the model's tokenizer and reserve input_cost + max_tokens × output_price in Redis with a Lua script that atomically checks spent + reserved + this ≤ limit. After the call, settle: subtract the reservation, add the real cost from the provider's usage field (which also reports cached input tokens at their discounted price). Reservations carry a TTL-backed record so a crashed gateway does not leak reserved money forever; a sweeper releases reservations older than the max call duration. This bounds overshoot to roughly the in-flight error of a few requests. Keep two levels: a soft limit that alerts or downgrades the model, and a hard limit that returns 429. Also enforce provider-side limits (tokens per minute per deployment) separately, because those protect your rate quota, not your money. Use the provider's returned usage for billing, never only your own estimate.
Failover that does not change behaviour silently
Failover has three traps. First, mid-stream failure: once you have sent tokens to the caller you cannot transparently retry, because the second model will write a different answer. So retry or fail over only before the first token; after that, end the stream with an error event and let the caller decide. Use a separate TTFT timeout (e.g. 4 s) and a total timeout (e.g. 60 s) so a hung provider is detected early. Second, which errors to fail over on: 429 and 5xx and timeouts, yes; 400 (bad request, context too long) and content-policy refusals, no, because the fallback will fail the same way or behave differently. Third, model equivalence: a prompt tuned for model A may produce worse output on B. Only list fallbacks that passed the feature's offline eval set, and store the fallback chain per feature, not globally. Circuit breakers should be per deployment (provider + model + region + key), not per provider, so one bad Azure region does not take down the healthy ones. Add jittered backoff and a retry budget (retries ≤ 10% of traffic) so a provider brownout does not get a retry storm from 50 gateway pods.
Spreading load across provider quotas
Providers enforce requests-per-minute and tokens-per-minute per key, project or deployment. At 1,500 req/s with 2,300 tokens each you need about 200M tokens per minute, far more than one key allows. So the gateway holds a pool of deployments per model (several regions, several keys) and routes by remaining quota, not round robin. Each deployment has a token bucket in Redis refilled at its TPM; the router picks the deployment with the most remaining tokens whose breaker is closed, and treats provider 429s with a Retry-After as a signal to pause that deployment. Add priority: interactive features (chat a user is waiting on) get first claim on quota, while batch jobs (nightly summarisation) use a separate pool or the provider's batch API at a large discount. When total demand exceeds total quota, shed the batch traffic first. For India-residency tenants the candidate set is filtered to in-region deployments only, even if that means a 429 instead of failing over abroad; this is a compliance rule, not a performance choice.
Keeping the gateway off the critical-failure list
Centralising every LLM call creates a new single point of failure, so the gateway must be boring and stateless. Run it as many pods across three zones behind an L4 load balancer, with config held in memory and refreshed by push, so Postgres being down does not stop traffic. Redis is the only hot-path dependency: give every Redis call a 5 ms timeout and a fallback. If Redis is unreachable, skip the cache and switch budgets to a local in-memory limit of (remaining budget ÷ number of pods) for a few minutes; that is fail-open with a bound, written down in an ADR. Size for streaming: 10,000 open connections means file descriptors, memory per stream and idle timeouts matter more than CPU. Use an async server (Go, Rust, or Envoy-based), cap in-flight requests per pod with a semaphore and return fast 503s when full. Propagate client disconnects to the provider call so an abandoned stream stops costing money. Deploy with canaries and drain connections gracefully on shutdown, because a rolling deploy otherwise cuts thousands of streams.
8 Trade-offs
Build vs buy (LiteLLM, Portkey, cloud AI gateways)
Open-source and managed gateways already give an OpenAI-compatible proxy, multi-provider routing and basic budgets. Start with one if your scale and compliance allow it. Build (or heavily extend) when you need tight integration with internal auth, residency rules, custom cost allocation, or very high concurrency. In the interview, say you would start from an existing proxy and explain which parts you would own.
Central gateway vs client library
A library in each service avoids a network hop (~1-2 ms, small next to a 2 s LLM call) but spreads keys, budgets and routing logic across 40 deployments and makes provider changes slow. A central gateway gives one place for keys, cost and failover. The hop cost is negligible for LLM traffic, so the gateway wins; keep a thin client SDK only for retries and tracing headers.
Exact cache only vs semantic cache
Exact caching is always correct but hits rarely (5-10%) because prompts include user-specific context. Semantic caching hits more but can return a wrong answer for a similar-looking question. Make semantic caching an opt-in per feature with a measured false-hit rate, scoped by tenant; see the semantic cache design for details.
Fail open vs fail closed on budget store failure
Failing closed turns a Redis blip into an AI outage across the company. Failing fully open risks a runaway agent loop burning a day's budget. A local per-pod budget slice for a few minutes is the middle ground; for features with strict customer-paid quotas you may fail closed.
9 Bottlenecks
Provider quota, not gateway CPU, is the real limit
At 10× traffic the gateway scales horizontally, but provider TPM limits do not. Request quota increases early, add more deployments and regions, move batch work to batch APIs, route simple features to small or self-hosted models, and use prompt caching so repeated prefixes are cheaper and faster.
Hot budget key
One team with a single feature doing 800 req/s hammers one Redis key with reserve and settle calls. Batch locally: each pod takes a lease of budget (e.g. ₹500) from Redis and spends it in memory, renewing when low. Overshoot is bounded by pods × lease size.
Usage pipeline lag
At 50M events/day the aggregator falls behind and dashboards go stale. Partition Kafka by team, batch inserts into ClickHouse (10k rows per insert), and pre-aggregate per minute with materialised views.
10 Failure handling
One provider fully down for an hour
Breakers open within seconds, routes fail over to evaluated fallbacks, features with no fallback return a clear 503 and the product shows its non-AI fallback (e.g. 'summary unavailable'). Quota on the fallback provider becomes the constraint, so batch traffic is paused automatically.
Gateway pods crash mid-stream
Callers see a broken stream and retry with the same Idempotency-Key; reservations from the dead pod expire through the sweeper. Usage for the lost call may be missing, so a daily reconciliation compares gateway totals against provider usage exports.
Bad route config pushed
A typo routes a high-traffic feature to an expensive model. Config changes are validated (model exists, price within allowed tier), rolled out to 5% of pods first, and a cost-per-minute anomaly alert triggers auto-rollback to the previous version.
11 What impresses
- Explains why check-then-add budgets overshoot under concurrency and fixes it with atomic reserve and settle
- Refuses to fail over after tokens have streamed, and explains why
- Only lists fallback models that passed the feature's eval set
- Uses Little's Law to size concurrent streams and talks about file descriptors and memory, not CPU
- Separates provider quota protection from money budgets
- Mentions prompt-cache-friendly ordering (static system prompt first) as a cost lever
- Adds a daily reconciliation against provider usage exports
12 Mistakes
- Buffering the full response in the gateway, destroying time-to-first-token
- Retrying 400 errors or content-policy refusals on another provider
- One global circuit breaker per provider instead of per deployment
- Writing usage and cost synchronously to Postgres on the hot path
- Estimating tokens with a character count and billing teams from that estimate
- Making the gateway hold prompt templates and agent logic, turning it into a monolith
- Ignoring what happens when Redis or the config DB is down
13 Follow-ups
Lessons this design uses
Practice questions
- Five teams in your company call OpenAI and Anthropic directly from their own services, each with its own API key. Your manager asks whether you should build an internal LLM gateway. What do you say?
- Your primary LLM provider starts returning 503s and 429s for 20 minutes during peak hours. Walk me through how your system should behave.
- Your fintech app wants to send customer chat messages to a third-party LLM to draft replies. Messages contain names, phone numbers, PAN and account numbers. How do you design the PII handling?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.