Design under constraints
Five hard problems where the constraints fight each other — a latency budget, a cost ceiling, data that can't leave, an air-gap, a traffic spike. You can't have everything; the staff-level skill is naming the binding constraint, making the tradeoff explicit, quantifying it, and defending it. There is no right answer.
Learning objectives
- Identify which constraint binds first and drives the design.
- Turn a conflicting constraint set into a concrete architecture.
- Make every tradeoff explicit — and state what capability you give up.
- Quantify a tradeoff with a small model instead of hand-waving.
- Defend a design against its failure modes as a tech lead would.
Each scenario below is a real staff-interview / design-review shape: a set of constraints that cannot all be satisfied. Work them in order. For each one, the pattern is the same — find the constraint that binds first (the one that forces your hand), design to it, then write down, out loud, what you traded away. A design with no stated tradeoff is not finished; it just hasn't found its cost yet.
The move: which constraint binds first advanced
Before any boxes-and-arrows, ask which single constraint you'd break first if you had to. That constraint binds — it forces a structural choice, and everything else bends around it. Latency binds → you can't multi-hop. A cost ceiling binds → you route cheap-first. Data residency binds → the frontier API is off the table. Read the chain below left-to-right: the binding constraint forces a design, which costs you a capability.
This is the mental model behind every scenario in the lesson. Read it left to right — it's the reasoning you run before drawing any architecture.
- Constraint set — the raw problem: several requirements that fight each other (fast and cheap and private, say). You usually cannot satisfy all of them.
- Which binds first? — the key question. Ask which single constraint you'd be forced to break first. That one binds: it decides the shape of everything else.
- Forces a design — the binding constraint makes a structural choice for you (e.g. latency binds → no multi-hop; residency binds → self-host). You don't get to pick freely anymore.
- Costs a capability — the design you were forced into gives something up. That sacrifice is the tradeoff, and naming it out loud is the whole exercise.
In short: Pick any real system you've built and ask 'which constraint would I break first?' The answer is the constraint that was quietly driving your whole design.
Scenario 1 · A 300 ms p99 latency budget advanced
Constraints. A typeahead-style feature must answer within a 300 ms p99 budget, end to end, including network. Accuracy matters but the product is unusable if it's slow. Volume is moderate; cost is not the tight constraint here.
Binding constraint: latency. The tension: the most accurate setup — a large frontier model doing multi-hop reasoning with a retrieval round-trip — is exactly what blows a 300 ms budget. Every accuracy lever you'd reach for adds milliseconds.
Worked design. A 300 ms p99 budget has to be allocated, not hoped for. Spend it: leave headroom for network + serialization, cap the model's share, and make retrieval optional. That forces a small/distilled model, a single hop (no agentic loop), and an exact-match cache in front so hot queries never touch the model at all. Model the allocation before you commit to it.
latency_budget.pydef allocate(budget_ms, fixed, model_p99):
"""fixed: dict of unavoidable costs (network, serialize, retrieval).
Returns the ms left for the model and whether the chosen model fits."""
reserved = sum(fixed.values())
for_model = budget_ms - reserved
return {"reserved_ms": reserved, "left_for_model_ms": for_model,
"model_fits": model_p99 <= for_model,
"over_by_ms": max(0, model_p99 - for_model)}
fixed = {"network_rt": 40, "tls_serialize": 20, "retrieval": 60}
print("frontier (350ms):", allocate(300, fixed, model_p99=350))
print("distilled (140ms):", allocate(300, fixed, model_p99=140))
# Drop retrieval to buy the model more room:
print("distilled, no RAG:", allocate(300, {"network_rt": 40, "tls_serialize": 20}, 140))
frontier (350ms): {'reserved_ms': 120, 'left_for_model_ms': 180, 'model_fits': False, 'over_by_ms': 170}
distilled (140ms): {'reserved_ms': 120, 'left_for_model_ms': 180, 'model_fits': True, 'over_by_ms': 0}
distilled, no RAG: {'reserved_ms': 60, 'left_for_model_ms': 240, 'model_fits': True, 'over_by_ms': 0}
This helper turns a latency budget from a hope into an allocation. You have a fixed number of milliseconds; this function subtracts the unavoidable costs and tells you how many are left for the model — and whether the model you want actually fits.
fixedis a dictionary of costs you can't avoid on the request path — network round-trip, serialization, an optional retrieval hop.sum(fixed.values())is everything spent before the model even starts.for_model = budget_ms - reservedis the milliseconds left for inference.model_p99is the model's own p99 latency;model_fitsis just the honest comparison of the two.- The three calls show the design search: a frontier model (350 ms) overshoots; a distilled model (140 ms) fits; and dropping retrieval frees another 60 ms of headroom — the exact tradeoff the scenario makes.
What the output means: The frontier model is over by 170 ms — off the table. The distilled model fits with room to spare, and cutting the retrieval hop buys even more. That's the latency constraint forcing a smaller model and fewer hops.
Try this: Add a 'reranker': 30 entry to fixed and re-run — watch the distilled model's headroom shrink. Every feature you add to the path spends budget the model no longer has.
The tradeoff, explicit. The frontier model overshoots by 170 ms — it is simply not an option at this budget. You ship the distilled model and trade peak accuracy for the budget: on the hardest queries the small model will be worse, and you accept that. What you give up: multi-hop reasoning, and — if you drop retrieval to buy headroom — freshness/grounding on the long tail. Failure mode: a cache miss on a cold query still has to fit 300 ms; size the cache and the model for the miss path, not the hit path, or your p99 lies to you.
Scenario 2 · $500/month hard ceiling at 1M requests advanced
Constraints. A summarization endpoint serves 1,000,000 requests/month under a hard $500/month ceiling — not a target, a wall. Quality should be as high as the money allows. Latency is comfortable.
Binding constraint: cost. The tension: $500 / 1M = $0.0005 per request. A frontier model on a real summarization prompt costs multiples of that. You cannot send every request to the good model and stay under the wall.
Worked design. A cascade: a cache absorbs repeats for free; a cheap small model handles the easy majority; only low-confidence cases escalate to the expensive model. The whole design lives or dies on the blended per-request cost landing under $0.0005 — so compute it, don't guess.
routing_cost.pydef blended_cost(n, ceiling, cache_hit, esc_rate, cheap_cost, exp_cost):
"""n requests. cache hits are free; misses go cheap, a fraction escalate to expensive."""
misses = n * (1 - cache_hit)
cheap = misses * (1 - esc_rate) * cheap_cost
expensive = misses * esc_rate * exp_cost
total = cheap + expensive
per_req = total / n
return {"monthly_$": round(total, 2), "per_req_$": round(per_req, 6),
"under_ceiling": total <= ceiling, "headroom_$": round(ceiling - total, 2)}
# 1M req, $500 wall. 30% cache, 15% escalate; cheap=$0.0002, expensive=$0.004
print("cascade:", blended_cost(1_000_000, 500, 0.30, 0.15, 0.0002, 0.004))
# Naive "everything to the expensive model":
print("all-expensive:", blended_cost(1_000_000, 500, 0.0, 1.0, 0.0002, 0.004))
cascade: {'monthly_$': 539.0, 'per_req_$': 0.000539, 'under_ceiling': False, 'headroom_$': -39.0}
all-expensive: {'monthly_$': 4000.0, 'per_req_$': 0.004, 'under_ceiling': False, 'headroom_$': -3500.0}
This models whether a routing cascade fits a hard cost ceiling. Free cache hits, a cheap model for the majority, and an expensive model for a small escalating fraction — the question is whether the blended per-request cost lands under the wall.
misses = n * (1 - cache_hit)— cache hits cost nothing, so only misses reach a model. Of those, most go to the cheap model and a fraction (esc_rate) escalate to the expensive one.totalsums the cheap and expensive spend;per_reqdivides by all requests to get the blended cost that must beat the ceiling.- The honest result: the first cascade is $39 over the $500 wall — the model tells you a 15% escalation rate is too generous. All-to-expensive is 8× over and never in contention. This is quantifying the tradeoff instead of guessing.
What the output means: under_ceiling: False, headroom_$: -39.0 means the plan doesn't fit yet. You tighten a knob — more cache, less escalation — until headroom goes positive. The number failing is the point: it's how you find the real ceiling.
Try this: Lower esc_rate to 0.10 and re-run — watch headroom flip positive. That's the exact quality-for-cost trade the scenario makes explicit: fewer borderline cases get the strong model.
Read the numbers. All-expensive is 8× over the wall — never in contention. But the first cascade is also over, by $39: a 15% escalation rate is too generous. This is the honest part of the exercise — the model tells you the plan doesn't fit yet. You have three knobs: raise the cache hit rate, lower the escalation threshold (send fewer to the expensive model), or find a cheaper 'expensive' tier. Tightening escalation to ~10% or cache to ~35% brings it under.
The tradeoff, explicit. You trade quality on the escalation margin for the ceiling: the borderline cases that would have gone to the strong model now get the cheap one, and some of those answers are worse. What you give up: a slice of top-end quality, and the simplicity of one model — a cascade is more moving parts to monitor. Failure mode: the escalation rate is not fixed. If input difficulty drifts up, escalation climbs, and you silently blow the ceiling — so the escalation rate must be a monitored, alerted metric, and you need a load-shedding fallback (serve cheap-only) when the month's budget is spent.
Scenario 3 · No data may leave our network expert
Constraints. Regulated data (health / financial). No request payload may leave the corporate network — a compliance/legal wall, not a preference. The task is hard reasoning over sensitive documents.
Binding constraint: data residency. The tension: the most capable models are closed APIs that require sending the payload out. Residency removes them from the board entirely, regardless of how much better they are.
Worked design. A self-hosted open-weight model on infrastructure you control — the data never crosses the boundary. Everything follows from that one forced move: you now own GPUs, a serving stack (vLLM/TGI), quantization to fit the hardware, capacity planning, and an on-call for the model itself. Route only the non-sensitive metadata (if any) to a stronger API; the sensitive path stays in-house, full stop.
| Axis | Closed API (banned here) | Self-hosted open (forced) |
|---|---|---|
| Data residency | leaves the network ✗ | stays in-house ✓ |
| Capability | frontier | trails on the hardest reasoning |
| Ops burden | none | you own GPUs + serving + on-call |
| Cost shape | per-token, elastic | fixed GPU cost, flat at volume |
The tradeoff, explicit. You accept a capability gap to keep the data in-house. The open model is measurably weaker on the hardest reasoning, and you pay a standing ops cost that an API doesn't have. That is not a bug in the design — it is the price of the constraint, and it's the right price to pay because the alternative is illegal. What you give up: peak reasoning quality and zero-ops simplicity. Failure mode: a subtle exfiltration path — logs, traces, error payloads, or a third-party observability SaaS — quietly ships the very data you self-hosted to protect. The design isn't done until the whole telemetry path is inside the boundary too.
Scenario 4 · Must work offline / air-gapped expert
Constraints. The system runs on a machine with no internet — a ship, a secure facility, a field laptop. No API calls, no web search, no package pulls at runtime. It must answer usefully with what's on the box.
Binding constraint: connectivity (there is none). The tension: almost every modern LLM convenience — hosted models, server-side tool use, live retrieval, remote MCP servers — assumes a network. Air-gap deletes all of them at once.
Worked design. Everything bundled, local, ahead of time. A quantized open-weight model sized to the on-box hardware (GGUF via llama.cpp/Ollama, or a small vLLM). Any 'tools' are local functions only — file readers, a calculator, a local SQLite — never a network call. RAG becomes a pre-built local index shipped on disk (no live search). Model weights, index, and dependencies are all vendored into the image before it goes offline.
Air-gap build checklist
- Pick a model that fits the hardware quantized — 4-bit if VRAM is tight (accept the quality hit).
- Vendor the weights + tokenizer into the image; no download at runtime.
- Replace every server/tool call with a local equivalent, or drop the feature.
- Pre-build the retrieval index offline and ship it on disk; refresh only on a manual re-image.
- Pin and vendor all dependencies — assume
pip installwill fail at runtime.
The tradeoff, explicit. You trade freshness and top-end capability for autonomy. The model's knowledge is frozen at bundle time; the index goes stale until someone physically re-images the box; the local model is smaller than anything you'd call via API. What you give up: live information, server-side tools, and easy updates. Failure mode: hidden network assumptions — a library that phones home, a tokenizer that lazy-downloads, a 'tool' that quietly hits a URL — turn into hangs or crashes in the field. Test the whole thing with the network physically pulled, not just firewalled.
Scenario 5 · A 10× traffic spike is coming tech-lead
Constraints. A launch/seasonal event will push 10× normal traffic for a few hours. You cannot provision 10× GPUs permanently (cost), and you cannot fall over. The product owner will accept degraded service but not an outage.
Binding constraint: peak capacity vs standing cost. The tension: sizing for the peak wastes money 99% of the time; sizing for the average falls over at the peak. GPU capacity is slow and expensive to scale, so you can't just autoscale your way out in seconds.
Worked design — the tech-lead move. Don't pick a single point on the curve; design a system that degrades in defined stages instead of collapsing. Layer four defenses: a request queue to absorb bursts (trade latency for survival), autoscaling on GPU replicas within a pre-warmed ceiling (accept it's slow), graceful degradation (shrink max_tokens, drop optional RAG, route everything to the cheap/small model under load), and finally load-shedding — reject or 503 the lowest-priority traffic to keep the core alive. Each layer is a tradeoff you pre-decide, so the system chooses correctly under stress instead of a human panicking at 2 a.m.
This diagram is the tech-lead answer to a 10× spike: not one capacity choice, but a ladder of pre-decided sacrifices. Read it top to bottom as load climbs — each rung trades something away so the next rung is the last resort, not the first.
- Load rises — the trigger. Traffic is heading past what your steady-state capacity can serve well.
- Queue (add latency) — the first, cheapest trade: buffer bursts and make users wait rather than fail. You trade latency for survival.
- Autoscale to ceiling — add GPU replicas up to a pre-warmed cap. It helps but is slow and bounded, which is why it's not your only defense.
- Degrade quality — shrink
max_tokens, drop optional RAG, route to the small/cheap model. Users get worse answers instead of no answers. - Shed low-priority — the last resort: reject or 503 the least-important traffic to keep the core alive. An outage for some beats an outage for all.
In short: The order is the whole point: latency first, then quality, then completeness — availability last. Decide this ladder before the spike so the system, not a panicking human, makes the call at 2 a.m.
The tradeoff, explicit. You trade latency, then quality, then completeness — in that order — to never trade availability. Under the spike, users wait longer, get shorter answers from a smaller model, and the least-important requests get turned away. That is a chosen, defensible sequence, not a random failure. What you give up: uniform quality during the peak and some standing cost for the pre-warmed ceiling and queue infra. Failure mode: if you never rehearse it, the degradation paths rot — the 'small model' route 500s, the queue has no max depth and OOMs, load-shedding drops the wrong tier. Degradation is only real if you load-test it before the event.
Grade your design tech-lead
Score any design you produced for these scenarios against the dimensions below. This is the same rubric a design review uses — the goal is not a 'correct' architecture but a defensible one.
| Dimension | Meets bar | Above bar |
|---|---|---|
| Named the binding constraint | identifies which constraint forces the design | shows why it binds before others and what order the rest fall in |
| Made the tradeoff explicit | states what is sacrificed for the constraint | states it in the design's own terms and where it hurts most |
| Quantified it | puts a rough number on the cost/latency/gap | models it (budget/cost/break-even) and reads the number honestly, even when it fails |
| Considered failure modes | names the main way the design breaks | covers the non-obvious ones (miss path, telemetry leak, hidden network, untested degradation) |
| Defensible | could justify the call to a skeptic | pre-empts the strongest objection and says what would change the decision |
Meets bar on all five = a solid design. Above bar on three or more = staff-level. If you can't name the binding constraint or the tradeoff, you haven't finished — go back and find what the design gave up.
You're handed a design with a 300 ms p99 budget, a frontier model, and a live retrieval hop, and told 'it's fast enough on average.' What's the first thing you check, and why is 'fast on average' not the answer?
Show answer
A team self-hosts an open model to satisfy 'no data leaves our network,' then ships prompts to a hosted logging/APM service for observability. Has the constraint been met? What does this teach about identifying the binding constraint?
Show answer
Exercise AC3.1 — Design under a constraint you're handed
Context: A design is judged not on being ‘correct’ but on being defensible: you name the binding constraint, quantify the tradeoff with a tool, and say what you gave up. This exercise runs the §1 framework end to end.
Your task: Take one scenario (or invent a tight constraint set) and produce a one-page design that names the binding constraint, quantifies the tradeoff, and grades itself.
Requirements:
- Name the single binding constraint you design against first
- Use
latency_budget.pyorrouting_cost.pyto quantify the tradeoff (extend them if needed) - Write the sentence ‘I trade ___ for ___, and I give up ___.’
- List at least two failure modes of your design
- Grade yourself with the rubric and note where you fell to ‘Meets’ instead of ‘Above’ — that gap is your next iteration
💡 Hint: The quantified number is what separates a defensible design from an opinion — run the tool and let it, not intuition, size the tradeoff.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Under conflicting constraints, the amateur optimizes the easy dial and the professional first finds the wall. Some constraints are tunable; one is usually binary and rules out whole architectures.
Your task: Given a system that must answer under 300 ms p99, cost under $500/mo, and never send data off-network, decide which constraint you design against first and justify it.
Requirements:
- Identify the binding constraint as the one hardest to relax and cheapest to violate accidentally (here: no data egress)
- Explain why it's binary and rules out whole architectures (hosted LLM APIs), unlike latency and cost
- Note that latency and cost are tunable (cache, batch, smaller model) — dials, not walls
- State the sequencing: satisfy the wall first, then optimize the dials within that box
💡 Hint: Ask which constraint, if violated, is a compliance incident rather than a slow response — solve that one first.
Show solution
Design against the constraint that is hardest to relax and cheapest to violate accidentally. Here that is no data egress: it is binary (you either comply or you don't), it rules out entire architectures (hosted LLM APIs), and violating it is a compliance incident, not a slow response.
Latency and cost are tunable — you can cache, batch, or pick a smaller model. Egress is a wall. So: pick the deployment model that satisfies egress first (self-hosted / in-VPC), then optimize latency and cost within that box. Solve the wall before the dials.
Context: A latency budget is a design tool, not a hope: you allocate the milliseconds across stages, find where the time actually goes, and attack the dominant stage. For RAG answers, generation dominates.
Your task: Allocate a 300 ms p99 end-to-end budget across the RAG pipeline stages and name the one technique that buys the most headroom.
Requirements:
- Give a rough per-stage allocation (embed, vector search, optional re-rank, generation, network/overhead) summing within budget
- Identify generation as the dominant stage and therefore the lever
- Name streaming as the biggest win — it moves the user-felt metric to time-to-first-token, not last
- Add a secondary lever (cap output tokens / smaller-faster generation model)
💡 Hint: Optimize what the user actually perceives: with streaming, perceived p99 can sit far below the true end-to-end number.
Show solution
Budget the pipeline explicitly (rough allocation):
| Stage | Budget |
|---|---|
| Embed query | ~20 ms |
| Vector search (top-k) | ~30 ms |
| Re-rank (optional) | ~40 ms |
| LLM generation | ~180 ms |
| Network + overhead | ~30 ms |
Generation dominates, so it is the lever. Biggest headwind-killer: stream the response so time-to-first-token, not time-to-last-token, is what the user feels — the p99 on perceived latency drops far below 300 ms. Second lever: cap output tokens and use a smaller/faster model for the generation step; retrieval is already cheap.
Context: A hard cost ceiling turns into arithmetic: $500/mo over 1M requests is $0.0005 per request, which is below a per-call hosted generation. The math itself forces the architecture.
Your task: Show whether calling a hosted LLM on every request is viable at $0.0005/request and give the architecture that makes the number work.
Requirements:
- Do the arithmetic and conclude you cannot call the model on every request
- Introduce aggressive caching so only cache-misses hit the model (state the effect on effective cost)
- Add model tiering (cheap small model for easy queries, escalate the hard ones) and a hard output-token cap
- Give the verification formula (miss_rate × avg_tokens × price) and confirm it lands under the ceiling before committing
💡 Hint: Caching is what converts an infeasible ceiling into a feasible one — estimate the hit rate, then check the miss cost against the budget.
Show solution
$0.0005/request is below the cost of even a small hosted generation call on every request, so you cannot call the model on every request. The math forces a caching architecture.
- Cache aggressively: if 60% of queries are semantically near a prior query, serve them from a cache at ~$0. Only the 40% cache-misses hit the model, cutting effective cost by more than half.
- Tier the models: route easy/short queries to a cheap small model, escalate only the hard ones. Most traffic is easy.
- Batch + cap tokens: hard-limit output length; verbose answers are the silent budget killer.
Verify: model_cost = miss_rate × avg_tokens × price. Plug in your cache hit rate and confirm it lands under $500 before committing. Caching is what turns an infeasible ceiling into a feasible one.
Context: ‘No data leaves our network’ sounds simple until you remember the whole data path, not just the model call. The leaks people miss are in telemetry and in the agent's own tools.
Your task: Give the architecture for a strict no-egress rule and the two subtle leaks people forget.
Requirements:
- Architecture: self-host an open-weight model in-VPC/on-prem, self-host the vector store and embedding model, keep all traffic inside the boundary
- Leak 1: telemetry & logs (error trackers, APM, prompt-logging SaaS) that ship content to a vendor
- Leak 2: third-party tools the agent calls (web search, translate) that exfiltrate through a side door
- State the review question: does any byte of customer data reach a network you don't control?
💡 Hint: Audit every SDK and tool that sees request data, not just the LLM call — compliance is about the whole path.
Show solution
Architecture: self-host an open-weight model in-VPC (or on-prem GPU), self-host the vector store and embedding model, keep all traffic inside the network boundary. No third-party API in the request path.
The subtle leaks:
- Telemetry & logs: error trackers, APM, and prompt-logging SaaS silently ship prompt/response content to a vendor. Audit every SDK that sees request data, not just the LLM call.
- Third-party tools the agent calls: if the agent has a ‘web search’ or ‘translate’ tool backed by an external API, it exfiltrates content through the side door. Every tool is a potential egress point.
Compliance is about the whole data path, not just the model call. The review question is ‘does any byte of customer data reach a network we don't control?’
Context: Air-gapped is a stricter world than no-egress: there is no internet at all and an ops team that can't pull updates on demand. The artifact becomes the product.
Your task: Give the design and operational plan for a system that must run air-gapped and be maintained by an ops team that can't fetch anything at runtime.
Requirements:
- Everything local: model weights, embedding model, vector index, and dependencies vendored into the deploy artifact (no installs, no downloads, no phone-home license checks)
- Ship one immutable, versioned artifact so staging is byte-identical to prod
- Update only via a full new artifact validated in a connected staging mirror, then carried across the gap — no in-place patching
- Keep observability on-box and ship runbooks + a safe fallback in the bundle for graceful degradation
💡 Hint: Assume no runtime dependency is reachable: if it isn't in the bundle, it doesn't exist at run time.
Show solution
Design: everything local — model weights, embedding model, vector index, and dependencies vendored into the deployment artifact. No package installs, no model downloads, no license checks that phone home at runtime.
- Immutable artifact: ship a single versioned image/bundle containing weights + code + deps. What ran in staging is byte-identical to prod.
- Update via physical media / approved transfer: new model versions arrive as a full new artifact, validated in a connected staging mirror, then carried across the air gap. No incremental in-place patching.
- Local observability: metrics and logs stay on-box with a local dashboard; ops reads them without egress.
- Degrade gracefully: if a component fails there is no ‘call support’ — ship runbooks in the bundle and a safe fallback answer.
Air-gapped means the artifact is the product; there is no runtime dependency you can assume is reachable.
Context: A 10x traffic spike in a week is a capacity problem you can't solve by linearly scaling GPUs. The professional answer is to scale what's cheap and design the degradation ladder before the event.
Your task: Produce the capacity plan and fallbacks for a confirmed 48-hour 10x spike on a RAG service currently at 60% capacity.
Requirements:
- Pre-warm the cache for the small set of queries the launch will hammer
- Horizontally scale the stateless tiers (API, retrieval) ahead of time, not reactively
- Add a per-user rate limit + bounded queue so overload degrades to ‘slower’ not ‘collapse’
- Define fallback tiers (smaller model on miss, shorter outputs, cached FAQ) as explicit switches set before the event
- Load-test to 10x now, in staging, to find the breaking component this week
💡 Hint: Decide the degradation ladder (normal → cache-first → shed load) in advance — graceful is a design decision, not a reflex during the incident.
Show solution
Reality: 60% headroom means 10x will melt it. You cannot linearly scale the LLM cost/GPU in a week, so the plan is scale-what's-cheap + shed-load-gracefully.
- Pre-warm the cache: the launch will hammer a small set of queries. Pre-compute and cache the top expected questions so the spike hits cache, not the model.
- Horizontally scale the stateless tiers: API and retrieval scale out easily; provision extra replicas ahead of time, not reactively.
- Rate-limit + queue: put a per-user rate limit and a bounded queue in front of generation so overload degrades to ‘slightly slower’ not ‘total collapse’.
- Fallback tiers: under extreme load, route to a smaller/faster model, shorten outputs, or serve a cached FAQ answer. Define these switches before the event.
- Load-test to 10x now: find the breaking component this week, in staging, not during the launch.
| Tier | Load response |
|---|---|
| Normal | Full RAG |
| High | Cache-first, small model on miss |
| Overload | Cached FAQ + queue, shed excess |
Lesson: plan the degradation ladder before the spike; graceful is a design decision, not a reflex.
✓ Checkpoint — you can move on when you can…
- Identify which constraint binds first and explain why it forces the design.
- Turn a conflicting constraint set into a concrete architecture with the tradeoff named.
- Quantify a latency budget and a routing cost — and read the number even when it fails.
- State the failure modes (miss path, telemetry leak, hidden network, untested degradation).
- Grade a design as defensible, not 'correct,' and say what you gave up.