Design problems
AI-era systemsHardSDE-3 35 min

Design an LLM gateway with failover and budgets

Asked at
FlipkartFreshworksMicrosoftSalesforceSwiggyZoho

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

OpenAI-compatible provider abstractionvirtual keys, real keys only in the gatewayreserve-then-settle token budgetscircuit breaker per deployment, failover only before first tokenrouting by remaining TPM/RPM quotastreaming passthrough with cancellation on disconnectexact cache, optional tenant-scoped semantic cacheasync usage events to Kafka and ClickHouseversioned price tabledata-residency routinggateway is stateless and fails safe when Redis is down

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

Request rate~60/s average, 1,500/s peak5M LLM calls/day ÷ 86,400 ≈ 58/s; product spikes (sale day, batch jobs) and daily peaks give up to ~25× → plan for 1,500/s
Concurrent streams~10,000 at peakLittle's Law: 1,500/s × ~7 s average call duration ≈ 10,500 open connections
Tokens per day~10B input, ~1.5B output5M calls × ~2,000 input tokens (system prompt + context) and ~300 output tokens
LLM spend~$50k/day (~$1.5M/month)At an illustrative $3/M input and $15/M output: 10B × $3/M = $30k + 1.5B × $15/M = $22.5k per day
Savings target25-40%Exact cache ~5-10% of calls, provider prompt caching on stable prefixes (cached input billed at a large discount), routing simple features to small models
Usage events5M/day, ~5 GB/day~1 KB per event (tenant, feature, model, tokens, cost, latency, cache flags) → ClickHouse, compresses ~10×
Prompt log storage~50 GB/day raw5M × ~10 KB redacted prompt + response; keep 30 days in S3 → ~1.5 TB, sampled for most features

3 API

EndpointWhat it does
POST/v1/chat/completionsOpenAI-compatible chat call. Headers: Authorization (virtual key), X-Feature, X-Tenant-Id, Idempotency-Key. stream=true returns SSE.
POST/v1/embeddingsEmbedding 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/keysIssue or rotate a virtual key scoped to a team, features and models.

4 Data model

routesPostgres → pushed to gateway memory
teamfeature (PK pair)primary_modelfallbacks[]max_tokensttft_timeout_mstotal_timeout_mscache_policyresidencyversion

Low 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 + memory
deployment_id (PK)providermodelregionapi_key_ref (secret manager)tpm_limitrpm_limitprice_version

One model can have many deployments (Azure regions, several org keys). The router load-balances across them by remaining quota.

virtual_keysPostgres + local cache
key_hash (PK)teamallowed_features[]allowed_models[]statuscreated_at

Only the hash is stored. Cached in gateway memory for 60 s so revocation takes effect within a minute.

budget_countersRedis Cluster
budget:{team}:{feature}:{day} → spent_micro_usdreserved_micro_usd; TTL 35 days

Atomic Lua for reserve/settle. Hash tag on team keeps one team's keys on one shard.

exact_cacheRedis
key = sha256(model, params, system, messages, tools) → responseusage; TTL per feature

Only for temperature 0 or features that allow reuse. Scoped by tenant inside the hash.

usage_eventsKafka → ClickHouse
request_idtsteamfeaturetenantmodeldeploymentinput/output/cached tokenscost_micro_usdttft_mstotal_msstatuscache_hitfailover_from

Columnar store for cost dashboards and anomaly alerts; partition by day, order by (team, feature, ts).

price_tablePostgres
modelprice_versioninput_per_moutput_per_mcached_input_per_mvalid_from

Versioned so old calls are never re-priced with today's price.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALHTTPS + virtual keyreserve budget, cachestream (SSE)on 429/5xx/timeoutcheap featuresusage eventcost rowsredacted logsroutes, budgetsconfig pushProduct servicesInternal LB (L4)Admin + config APILLM gateway(stateless)Cost aggregatorKafka: usage eventsClickHouse usagePostgres: routes,keysRedis: budgets +cacheS3: redacted promptlogsProvider A (primary)Provider B (fallback)Self-hosted model(vLLM)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

A streamed chat call

  1. 1The support-summary service sends POST /v1/chat/completions with its virtual key, X-Feature: ticket-summary and stream=true.
  2. 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.
  3. 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.
  4. 4It checks the exact cache (temperature 0, cacheable feature). Miss.
  5. 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.
  6. 6The first token arrives in 600 ms; the gateway flushes each SSE chunk to the caller as it arrives, never buffering the whole answer.
  7. 7At the end the provider's usage block gives exact tokens (including cached input). The gateway settles the Redis reservation to the real cost.
  8. 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

  1. 1Provider A starts returning 503 and timeouts in one region.
  2. 2Each gateway instance tracks errors per deployment in a sliding window; after 50% errors over 20 requests the breaker opens for 30 s.
  3. 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.
  4. 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.
  5. 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

  1. 1The search team's daily budget is ₹4 lakh with a soft limit at 80%.
  2. 2At 80% the aggregator alerts the team on Slack and, if configured, the route switches to the downgrade model for low-priority features.
  3. 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.
  4. 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

Q1How would you route a tenant's traffic only to India-region endpoints?
Q2How do you stop a runaway agent loop from burning a team's monthly budget in an hour?
Q3How would you support tool calling and structured output across providers with different APIs?
Q4How would you A/B test a cheaper model for one feature safely?
Q5What changes if half of the traffic moves to self-hosted models on your own GPUs?
Q6How do you attribute cost to end customers of a multi-tenant SaaS product?

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.

Start free