Routing, Failover & Backpressure: The Core Systems Round
The three flagship projects, designed for real - gateway, resilient routing, admission control
Designing the inference gateway
After this you can design a provider-abstracting gateway where adding a model is a config change
Every Tab completion, every Agent step, every chat turn in Cursor exits through one door. The inference gateway is that door and the JD describes the job as building a single abstraction over every provider's API so onboarding a new model is a config change, not a code change.
Anthropic, OpenAI, Google and a fleet of self-hosted models each speak a different dialect. Their auth differs, their request and response shapes differ, their streaming framing differs and their error vocabularies disagree about what a 429 even means. The gateway's first job is to swallow that heterogeneity so the rest of the system reasons about one model interface.
Frame it to an interviewer as an adapter pattern with teeth. A thin per-provider adapter translates the wire format. A fat shared core owns everything that should never be reimplemented per provider: timeouts, retries, tracing, token accounting, idempotency.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Everything above the adapter sees one model; the adapter is where the dialect dies.
What the gateway normalizesthe dialect problem
Name the specific seams that diverge across providers. Vague "it abstracts the API" reads as someone who has never onboarded a second provider. The texture below is what reads as having shipped one.
- Surface
- Auth
- How it diverges
- Header vs SDK token, per-org keys, rotating creds
- What the gateway exposes
- One credential resolver keyed by provider + region
- Surface
- Request shape
- How it diverges
- Message roles, tool schemas, sampling param names
- What the gateway exposes
- One canonical request DTO the adapter serializes
- Surface
- Streaming
- How it diverges
- SSE event names, delta framing, [DONE] sentinels
- What the gateway exposes
- One internal token-delta stream, provider-agnostic
- Surface
- Errors
- How it diverges
- 429 means rate-limit here, quota there, overload elsewhere
- What the gateway exposes
- A normalized error taxonomy: retryable, fatal, shed
- Surface
- Token accounting
- How it diverges
- Usage in trailer, in headers or estimated
- What the gateway exposes
- One usage record per request for cost + capacity
| Surface | How it diverges | What the gateway exposes |
|---|---|---|
| Auth | Header vs SDK token, per-org keys, rotating creds | One credential resolver keyed by provider + region |
| Request shape | Message roles, tool schemas, sampling param names | One canonical request DTO the adapter serializes |
| Streaming | SSE event names, delta framing, [DONE] sentinels | One internal token-delta stream, provider-agnostic |
| Errors | 429 means rate-limit here, quota there, overload elsewhere | A normalized error taxonomy: retryable, fatal, shed |
| Token accounting | Usage in trailer, in headers or estimated | One usage record per request for cost + capacity |
The adapter is where the dialect dies; everything above it sees one model.
The split: thin adapter, fat core
Decide deliberately what lives in the adapter versus the shared core. Put too much in the adapter and every provider reimplements retries badly. Put provider quirks in the core and you leak the abstraction.
Wire serialization and the SSE parser for this provider.
Auth handshake and credential format.
Mapping this provider's status codes onto the shared error taxonomy.
Timeouts, retry budgets, hedging, circuit breaking.
Tracing, metrics, structured logging, token accounting.
Idempotency keys and request dedup.
Streaming passthrough is first-class
The single biggest gateway mistake is buffering the full response before forwarding it. That destroys time-to-first-token, which is the latency a developer actually feels when text starts appearing in the editor.
- Forward token deltas as they arrive; never wait for the final chunk to start the client stream.
- Keep the gateway's per-request memory flat - proxy the stream, don't accumulate it.
- Tap the stream for metrics (TTFT, inter-token latency, token counts) without blocking forwarding.
- Propagate client cancellation downstream so an abandoned Agent turn stops burning provider tokens.
Adding a model is a config change
This is the line from the JD, so make it concrete. New models that share an existing provider's API should require zero code: a declarative entry naming the provider, model id, limits and pricing.
models:
claude-sonnet-tab:
provider: anthropic # reuses the existing adapter
upstream_id: claude-sonnet-4.6
surfaces: [tab]
limits: { rpm: 4000, tpm: 2_000_000 }
timeout_ms: 800 # tight: Tab is sub-100ms-budget critical
price_per_mtok: { in: 3.00, out: 15.00 }
fallback: claude-haiku-tabWhen they ask how you'd onboard a new provider, separate the two cases out loud: a new model on an existing provider is pure config; a new provider with a new API is one new adapter implementing a fixed interface plus its config. Naming that boundary is the whole answer - it proves you've found the right abstraction line instead of hand-waving "we'd abstract it."
A leaky abstraction is worse than no abstraction. If a provider's tool-calling semantics or stop-sequence behavior differ in ways callers must handle, surface that in the typed contract rather than silently papering over it. Pretending two genuinely different behaviors are identical produces bugs that only show up in production, on the hot path, for every user.
Takeaway. The gateway is a thin per-provider adapter over a fat shared core: adapters kill wire-format dialect, the core owns timeouts/retries/tracing/idempotency once and a new model on a known provider is a declarative config entry with no code.
Self-check
QWhere should retry logic live in the gateway - in each provider adapter or in the shared core - and why?
Cross-provider failover
After this you can design failover that hides single-provider outages from users
The JD sets a hard bar: no single provider outage causes user-visible degradation. When Anthropic browns out, Tab and Agent should keep working and most users should never notice.
Failover is layered defense, not one trick. Health-based routing keeps traffic off a sick provider before a request is even sent. Circuit breakers stop you from hammering a dying upstream. Bounded retries with jitter recover transient blips. A fallback ladder catches the rest. Each layer covers a failure the others miss.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Ranked by how broad an outage each layer can absorb - earlier layers prevent failures the later ones only mop up.
The resilience toolkitand what each one is actually for
- Technique
- Health checks
- Protects against
- Routing to a provider that's already down
- The trap
- Stale health: react in seconds, not minutes
- Technique
- Circuit breaker
- Protects against
- Pile-on retries against a dying upstream
- The trap
- Tune trip/half-open thresholds or you flap
- Technique
- Retry + jitter
- Protects against
- Transient 5xx / dropped connections
- The trap
- Uncapped retries become a self-inflicted DDoS
- Technique
- Fallback ladder
- Protects against
- A whole provider being unavailable
- The trap
- Silent quality drop if you don't track it
- Technique
- Request hedging
- Protects against
- A slow tail on one request
- The trap
- Duplicate spend if you hedge everything
| Technique | Protects against | The trap |
|---|---|---|
| Health checks | Routing to a provider that's already down | Stale health: react in seconds, not minutes |
| Circuit breaker | Pile-on retries against a dying upstream | Tune trip/half-open thresholds or you flap |
| Retry + jitter | Transient 5xx / dropped connections | Uncapped retries become a self-inflicted DDoS |
| Fallback ladder | A whole provider being unavailable | Silent quality drop if you don't track it |
| Request hedging | A slow tail on one request | Duplicate spend if you hedge everything |
Name the trap with each technique - that's what separates a reader from a builder.
Retries with a budget, not a count
The naive answer is "retry three times." The senior answer is a retry budget: cap retries as a fraction of total traffic so that when a provider fails broadly, retries can't multiply load and turn a partial outage into a total one.
- 1Classify the failure. Only retry what the normalized taxonomy marks retryable - a 503 or a timeout, never a 400 or a content-policy refusal.
- 2Back off with jitter. Exponential backoff plus randomization spreads the retry wave so a thousand clients don't all retry on the same tick.
- 3Spend from a global budget. If retries exceed, say, 10% of requests, stop retrying and shed - the provider is broadly down, not blipping.
- 4Cross over, don't just repeat. A retry that targets the same dead provider is wasted; route the retry to the next provider on the ladder.
Hedging: buy tail latency with cost
Hedging fires a second request to a different provider after a delay, then takes whichever responds first and cancels the loser. It directly attacks the slow tail and it directly costs duplicate spend.
- Trigger from data
- Fire the hedge at the measured p95, not a guess - so you only hedge the genuinely slow tail, ~5% of requests.
- Cap the blast radius
- Cancel the loser the instant the winner's first token arrives; never let both streams run to completion.
- Pick by surface
- Hedge Tab aggressively (cheap, latency-critical); hedge a long Agent turn rarely (a duplicate is expensive).
- Respect idempotency
- Two in-flight copies must dedup so you don't apply or bill the same effect twice.
Idempotency under retry and hedge
The moment you retry or hedge, the same logical request can hit upstreams twice. Without idempotency you double-charge the user, double-count tokens or double-apply a side effect.
- Stamp every logical request with an idempotency key at admission, before any retry or hedge can fork it.
- Dedup at the gateway: the first response to arrive wins, later duplicates are dropped, not forwarded.
- Make completions side-effect-free at the gateway layer so a duplicate is wasteful but never incorrect.
- Reconcile token usage against the winning response only, so cost accounting matches what the user actually received.
Quality-aware fallback
A fallback model is usually cheaper or faster and lower quality. That tradeoff is acceptable for Tab, where a slightly weaker completion beats no completion and far less acceptable for an Agent doing a multi-file refactor. Decide per surface and make the decision observable.
"I'd make fallback quality-aware and per-surface. Tab can drop to a smaller, faster model under a provider outage and the user barely notices a slightly weaker completion. Agent shouldn't silently degrade mid-task, so there I'd prefer to fail over to an equivalent-tier model on another provider and if none is healthy, surface a clear retryable error rather than quietly hand back worse work. Either way the fallback rate is a first-class metric, because a silent quality regression is the failure mode you find out about from a churn report instead of a dashboard."
If they push on "what if two providers go down at once," don't pretend the ladder is infinite. Say it explicitly: failover converts a hard outage into graceful degradation and past the last rung you shed load and return a clean, retryable error - you do not queue requests past the point they're useful. Knowing where graceful degradation ends is more senior than claiming it never does.
Takeaway. Failover is layered: health routing and circuit breakers keep traffic off sick providers, budgeted retries with jitter and a cross-provider fallback ladder recover the rest, hedging buys tail latency for cheap surfaces only and idempotency keys make every duplicate wasteful but never wrong.
Self-check
QWhy is a global retry budget safer than a fixed per-request retry count when a provider has a broad outage?
Backpressure and admission control
After this you can design load shedding and admission so spikes don't cascade
The JD frames this as making sure traffic spikes don't cascade into providers. Backpressure is the system saying no early and cleanly, so a surge doesn't melt the fleet or trip every provider's rate limiter at once.
The instinct under load is to queue everything. That's the trap. An unbounded queue trades a fast failure for a slow one: requests sit until they're stale, the client times out and retries and you've added load to a system that's already over capacity. Admission control decides what to accept before any of that happens.
The control surfacesfrom the edge inward
A fixed-depth queue with a clear reject-when-full policy.
Depth sized to latency budget: never queue past the point a response is still useful.
Cap in-flight requests per provider and per model.
The real ceiling is downstream capacity, so the limit lives here, not in hope.
Rate-limit by RPM and TPM to match provider limits.
Token-aware: a 100k-token Agent call costs far more than a Tab call.
When over budget, reject fast with a clean retryable error.
Shed low-priority work first to protect latency-critical traffic.
Shape traffic before the provider rejects it
Providers publish RPM and TPM limits. If you let traffic blow past them, the provider returns 429s, your retry logic kicks in and you've manufactured a retry storm out of your own success. The fix is to enforce the provider's budget proactively at the gateway.
- 1Model the provider's budget locally. Mirror each provider's RPM/TPM as a token bucket the gateway owns, refilled at the published rate.
- 2Admit against the bucket. A request that would exceed the budget is shed or queued briefly at the gateway - never forwarded to be rejected upstream.
- 3Reserve headroom. Run the bucket below the hard limit so a burst doesn't tip you over and so failover traffic from another provider has room to land.
- 4Adapt to reality. If the provider starts 429-ing despite your model, treat that as a signal to throttle harder, not to retry into it.
Priority and QoS classes
Not all traffic is equal. A Tab completion has a sub-100ms budget and a user waiting on a keystroke. A background re-index or a bulk eval job can wait. Under contention, the latency-critical class must win.
- Class
- Interactive / latency-critical
- Example surface
- Tab, ⌘K
- Under contention
- Protected - shed last, never queued past budget
- Class
- Interactive / tolerant
- Example surface
- Agent, chat
- Under contention
- Served, but may degrade to a fallback model
- Class
- Bulk / background
- Example surface
- Re-index, batch eval
- Under contention
- Shed first, deferred or rate-limited hard
| Class | Example surface | Under contention |
|---|---|---|
| Interactive / latency-critical | Tab, ⌘K | Protected - shed last, never queued past budget |
| Interactive / tolerant | Agent, chat | Served, but may degrade to a fallback model |
| Bulk / background | Re-index, batch eval | Shed first, deferred or rate-limited hard |
Fair queuing within a class; strict priority across classes - Tab does not wait behind a batch job.
Shedding a request cleanly is kinder than queueing it past its usefulness. A sub-100ms Tab request that sits in a queue for 400ms is already worthless when it dequeues - you spent capacity to deliver a stale answer the user has stopped waiting for. A fast, clear rejection lets the client back off intelligently; a slow death just guarantees a retry on top of the original load.
A shed request must return a non-cascading error. If load shedding produces a generic 500, clients treat it as retryable, retry immediately and you've built a feedback loop that amplifies the spike. Use a distinct, explicitly-retryable signal with a backoff hint so the client's reaction relieves pressure instead of adding to it.
Takeaway. Backpressure says no early and cleanly: bounded queues and concurrency limits cap in-flight work, local token buckets enforce provider RPM/TPM before upstream 429s and under contention you shed low-priority bulk traffic first to protect sub-100ms Tab - with a clean, explicitly-retryable error so shedding relieves pressure instead of amplifying it.
Self-check
Tail latency and the hot path
After this you can engineer for p99 on an always-hot request path
Cursor's request path is always hot and always user-facing. Optimize for the tail, because the p99 is what a developer feels - and for Tab, the whole budget is under 100ms, so the tail is the product.
The mean lies. If p50 is 40ms and p99 is 600ms, one in a hundred completions arrives after the user has typed three more characters and they experience that as broken even though the average looks great. Talk in percentiles or an inference interviewer will assume you've never operated a serving path.
Where the tail comes fromname it to mitigate it
- Source of tail
- Cold connections
- Why it spikes p99
- TLS + TCP handshake on first request to a provider
- Mitigation
- Connection pooling + keep-alive; pre-warm pools
- Source of tail
- GC / runtime pauses
- Why it spikes p99
- A stop-the-world pause lands on an unlucky request
- Mitigation
- Limit allocations on the hot path; tune the runtime
- Source of tail
- Head-of-line blocking
- Why it spikes p99
- A slow request stalls others behind it
- Mitigation
- Per-request isolation; bounded concurrency, not one queue
- Source of tail
- Queueing delay
- Why it spikes p99
- Request waits before any work starts
- Mitigation
- Shed early; size queues to the latency budget
- Source of tail
- Slow provider
- Why it spikes p99
- One upstream's tail bleeds into yours
- Mitigation
- Hedge to a second provider at measured p95
| Source of tail | Why it spikes p99 | Mitigation |
|---|---|---|
| Cold connections | TLS + TCP handshake on first request to a provider | Connection pooling + keep-alive; pre-warm pools |
| GC / runtime pauses | A stop-the-world pause lands on an unlucky request | Limit allocations on the hot path; tune the runtime |
| Head-of-line blocking | A slow request stalls others behind it | Per-request isolation; bounded concurrency, not one queue |
| Queueing delay | Request waits before any work starts | Shed early; size queues to the latency budget |
| Slow provider | One upstream's tail bleeds into yours | Hedge to a second provider at measured p95 |
Every tail source has a named fix - reciting the source without the fix is half an answer.
Keep the path lean
The gateway should add as little of its own latency as possible. The dominant cost is the provider's generation time, so the gateway's overhead has to be a rounding error, not a tax.
- Pool connections and keep them warm so no hot request pays for a handshake.
- Use async, non-blocking I/O end to end; one slow upstream must never starve a worker that other requests need.
- Never block the event loop with synchronous work - parsing, hashing or accounting on the critical path belongs off it.
- Stream straight through; buffering a response adds latency equal to the entire generation time.
Hedging is a measured threshold, not a vibe
Hedging is the strongest lever against a slow provider tail and it's only worth its cost if you fire it at the right moment. Set the trigger from the live latency distribution.
- Anchor on p95
- Fire the hedge when a request crosses the measured p95, so you only duplicate the slow ~5% - not every request.
- Recompute continuously
- The distribution drifts with provider health and time of day; a static threshold is wrong by lunch.
- Bound the extra spend
- A 5% hedge rate is ~5% extra cost on those requests - acceptable for Tab, often not for a long Agent turn.
- Cancel aggressively
- Kill the loser on the winner's first token so you pay for a sliver of overlap, not two full generations.
Budget the milliseconds end to end
Know where the time goes: client to gateway, admission and routing, gateway to provider, provider generation, stream back. For a sub-100ms Tab path, every hop has a number and you should be able to name which hop you'd cut first.
When asked to "make Tab faster," don't reach for a bigger machine. Ask what the p99 is and where it's spent, then attack the tail: pooled warm connections to kill handshakes, hedging at the measured p95 to cut the slow-provider tail and shedding to keep queueing delay out of the budget. Framing the answer as a latency budget you decompose, rather than "optimize the code," is the signal they're listening for.
Hedging everything inverts the goal. If you hedge at p50 instead of p95 you double your provider spend to shave a tail that wasn't slow and you can actually raise p99 by adding load. Hedging is a tail tool: it only pays when fired at the genuine slow minority.
Takeaway. Engineer for p99, not the mean: name each tail source (cold connections, GC, head-of-line blocking, queueing, slow providers) with its fix, keep the gateway's own overhead a rounding error with pooled warm connections and async I/O and hedge only at the measured p95 so you buy the slow tail without doubling spend.
Self-check
Observability, SLOs and safe rollout
After this you can instrument and operate the path like an owner
The JD asks for on-call-grade reliability on a path that's always hot. That means you operate it like an owner: explicit SLOs, an error budget, instrumentation that localizes a problem in minutes and rollouts that can't take everyone down at once.
Senior IC from week one means you don't wait for someone else to define "good." You name the SLOs, you wire the metrics and you build the kill switch before you ship the model that needs it.
SLOs you'd actually commit toper surface, not one global number
- Tab availability
- Very high - a failed Tab is a visible flicker on every keystroke; this is the strictest budget.
- Tab p99 latency
- Inside the sub-100ms budget end to end; this is the SLO Tab lives or dies on.
- Agent availability
- High, but a single retryable hiccup mid-task is more tolerable than a Tab miss.
- Cost per request
- A budget per surface, tracked like latency - a cheap-but-slow or fast-but-expensive regression both breach it.
Manage to an error budget: when you've burned it, you freeze risky rollouts until it recovers.
Metrics: golden signals plus inference-specific
The four golden signals (latency, traffic, errors, saturation) are table stakes. The inference-specific metrics are what tell an interviewer you know this domain.
- Metric
- TTFT
- What it tells you
- Time to first token - the latency users feel
- Alert when
- p99 breaches the surface's budget
- Metric
- Inter-token latency (ITL)
- What it tells you
- Stream smoothness during generation
- Alert when
- ITL spikes - provider is degrading mid-stream
- Metric
- Cache hit rate
- What it tells you
- Prefix/response cache effectiveness
- Alert when
- Drops sharply - a cost and latency regression
- Metric
- Failover rate
- What it tells you
- How often you leave the primary provider
- Alert when
- Rises - primary is unhealthy or quality-degrading
- Metric
- Shed rate
- What it tells you
- How much load you're rejecting
- Alert when
- Non-zero and sustained - you're under-provisioned
- Metric
- Per-provider error rate
- What it tells you
- Which upstream is sick
- Alert when
- One provider diverges from the pack
| Metric | What it tells you | Alert when |
|---|---|---|
| TTFT | Time to first token - the latency users feel | p99 breaches the surface's budget |
| Inter-token latency (ITL) | Stream smoothness during generation | ITL spikes - provider is degrading mid-stream |
| Cache hit rate | Prefix/response cache effectiveness | Drops sharply - a cost and latency regression |
| Failover rate | How often you leave the primary provider | Rises - primary is unhealthy or quality-degrading |
| Shed rate | How much load you're rejecting | Non-zero and sustained - you're under-provisioned |
| Per-provider error rate | Which upstream is sick | One provider diverges from the pack |
Failover rate and shed rate are the two metrics most people forget - they're where blast radius shows up first.
Tracing across providers
When a request is slow, you need to know which hop in seconds, not after an hour of log-grepping. A trace that spans admission, routing, the gateway adapter and the provider call localizes latency and failure immediately.
- Propagate a trace and idempotency id from admission through every retry, hedge and fallback so duplicated work is still one logical trace.
- Span each hop separately - routing decision, adapter serialize, provider TTFT, stream duration - so the slow hop is obvious.
- Tag spans with provider, model, surface and outcome (served / failed-over / shed) so you can slice the tail by any dimension.
Safe rollout of a new model or provider
A new model on the hot path can degrade quality or latency for everyone instantly. Roll it out so a mistake is contained and reversible in seconds.
- 1Shadow first. Mirror real traffic to the new model without serving its output; compare latency, cost and quality offline with zero user risk.
- 2Canary small. Route a tiny slice of live traffic, watch the per-surface SLOs and failover rate and hold before widening.
- 3Keep a kill switch. A config flag flips the model out of rotation instantly - rollback is a config push, not a deploy.
- 4Ramp on green. Widen the canary only while the error budget is healthy; freeze the ramp the moment it isn't.
Multi-region and multi-provider routing isn't just for failover - it caps how much a single bad change can break. A canary scoped to one region, one surface and one provider means a regression hits a sliver of traffic and a config-driven kill switch pulls it back before the error budget notices. Mention data-locality too: some traffic may be pinned to a region for compliance and your routing has to honor that constraint, not optimize around it.
Takeaway. Operate like an owner: per-surface SLOs (Tab availability and p99, cost-per-request) managed to an error budget; golden signals plus TTFT, ITL, cache-hit, failover and shed rate; cross-provider tracing that localizes the slow hop; and rollout via shadow → canary → config-driven kill switch so any regression is contained and reversible in seconds.
Self-check
QBeyond the four golden signals, which two inference-specific metrics most directly reveal that your blast radiusHow much breaks if a change goes wrong; the scope of potential damage. is growing and why?
A worked design: the request's journey
After this you can walk a single Tab/Agent request through the whole system
The strongest onsite design answer is a single request narrated end to end. Trace one completion from the keystroke to the streamed token and show where every technique from this module plugs in.
This is the backbone of the systems-design round. Walk the happy path once, then walk the failure branches, naming the mechanism at each step rather than gesturing at "the system handles it."
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Each stage owns exactly one class of failure - that's how the pieces compose without overlap.
The happy path, step by stepkeystroke to token
- 1Admission. The request arrives tagged with its surface and an idempotency key. Token bucket and concurrency check: under budget, admit; over budget, shed fast with a retryable error.
- 2Routing decision. Pick model and provider from config and live health, with cache affinity - route to where a matching prefix is already cached if there is one.
- 3Gateway adapter. Serialize the canonical request into the chosen provider's dialect, attach a tight timeout for the surface and open the upstream stream.
- 4Provider + stream. Tokens stream back through the gateway untouched - forwarded delta by delta to preserve TTFT, tapped for metrics, never buffered.
- 5Metrics + accounting. On completion, record TTFT, ITL, token usage and cost against the idempotency key; close the span.
Where each technique plugs in
- Stage
- Admission
- Technique
- Rate limit + load shed
- Failure it prevents
- Spike cascading into the provider
- Stage
- Routing
- Technique
- Prefix-cache affinity + health routing
- Failure it prevents
- Wasted recompute; routing to a sick upstream
- Stage
- Gateway
- Technique
- Timeout + hedge at p95
- Failure it prevents
- A slow provider blowing the latency budget
- Stage
- Provider call
- Technique
- Circuit breaker
- Failure it prevents
- Hammering a dying upstream
- Stage
- Retry path
- Technique
- Budgeted retry → fallback ladder
- Failure it prevents
- A provider outage becoming user-visible
| Stage | Technique | Failure it prevents |
|---|---|---|
| Admission | Rate limit + load shed | Spike cascading into the provider |
| Routing | Prefix-cache affinity + health routing | Wasted recompute; routing to a sick upstream |
| Gateway | Timeout + hedge at p95 | A slow provider blowing the latency budget |
| Provider call | Circuit breaker | Hammering a dying upstream |
| Retry path | Budgeted retry → fallback ladder | A provider outage becoming user-visible |
Each stage owns exactly one class of failure - that's how the pieces compose without overlap.
Tab versus Agent: same path, different dials
One pipeline serves both, but the parameters invert. Contrasting them shows you understand that the design is a set of dials, not a single fixed policy.
Tiny prompt, sub-100ms budget, latency-critical.
Hedge aggressively; tight timeout; smaller fallback model is fine.
Cheap enough that a duplicate from hedging barely registers.
Long, expensive, multi-step, cancellation-prone.
Hedge rarely; fail over to an equivalent-tier model, not a weaker one.
Propagate cancellation hard so an abandoned turn stops burning tokens.
The failure branches
The happy path is the easy half. Naming the branches - and the clean exit when there's no good option left - is what closes a strong design answer.
- Provider returns 5xx or times out → classify as retryable, spend from the retry budget, cross-route to the next provider on the ladder.
- Over admission budget → shed fast with an explicitly-retryable error and a backoff hint, never queue past the latency budget.
- Oversized context → down-route to a larger-window model or reject with a clear, actionable error before wasting a provider call.
- Last rung of the ladder is unhealthy → degrade gracefully per surface and if nothing is viable, return a clean retryable error rather than silently worse output.
"Let me trace one Tab request end to end. It's admitted against a token bucket - over budget, I shed fast with a retryable error rather than queue it past its 100ms life. Routing picks a model and provider by config and live health, preferring an upstream with a warm prefix cache. The gateway serializes to that provider's dialect with a tight timeout and a hedge armed at the measured p95. Tokens stream straight back, deltas forwarded untouched for TTFT, tapped for metrics. If the provider 5xxs, it's retryable so I spend from the budget and cross-route to the next rung of the fallback ladder; if the circuit's open, I skip it entirely. An Agent request runs the same pipeline with the dials inverted: hedge rarely, fail over to an equivalent-tier model and propagate cancellation hard so an abandoned turn stops paying for tokens."
Drive the whiteboard with the request, not the boxes. Most candidates draw components and describe them statically. Animate a single request through the system and let the components reveal themselves as it hits each one - admission, routing, gateway, provider, stream - then run a failure through the same path. That narrative is the difference between describing a diagram and demonstrating you've operated one.
Takeaway. Drive the design round by narrating one request: admission (rate-limit/shed) → routing (cache affinity + health) → gateway (timeout + hedge) → provider (circuit breaker) → stream + metrics, then run the failure branches; Tab and Agent are the same pipeline with the dials inverted.