AI EngineeringZero to ProductionHome·About·What’s new·Contact
Advanced Challenges · Part 3

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.

⏱️ ~2 hours🧪 5 scenarios🎯 Advanced→Tech-lead

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.

🧭 There is no right answer, only defensible tradeoffsNobody grades these on matching a hidden 'correct' architecture — because there isn't one. You are graded on whether you named the binding constraint, made the tradeoff explicit, quantified it, considered failure modes, and could defend the call. Two engineers can reach opposite designs and both be right — if both can say what they gave up and why it was worth it.

The move: which constraint binds first advanced

Here's the design review that goes wrong every week. A team is asked for "a fast, cheap, private, always-on assistant," so they open a whiteboard and start drawing the ideal system — best model, live retrieval, full observability — then spend the next hour discovering, one requirement at a time, that the ideal system violates every constraint they were given. By the end everyone is exhausted, the design is a compromise nobody chose on purpose, and the tradeoff that actually decided the architecture was never said out loud. The staff engineer in the room avoids all of that by asking one question first: which constraint binds?

Think of it like packing a suitcase for a flight. You have a weight limit, a size limit, and a list of things you want to bring. The amateur packs everything they'd like and then panics at the gate. The experienced traveler asks up front: which limit will I hit first? If it's weight, that decides what goes in — the heavy boots come out no matter how much you love them, and everything else is arranged around that one hard number. The binding constraint is exactly that: of all the requirements pulling against each other, it's the one that reaches its limit first and therefore forces a structural decision. Everything else is a dial you can still turn; the binding constraint is a wall you cannot move.

Why does naming it matter so much? Because a system has usually one — occasionally two — constraints that genuinely remove options from the board, and a long tail of constraints that are merely uncomfortable. Latency binding means you physically cannot afford a second network hop; it isn't "try to be fast," it's "the multi-hop architecture is dead." A hard cost ceiling means the expensive model can't be your default path, full stop. Data residency means the frontier API is not a candidate at any price. Confuse a dial for a wall — "let's just optimize cost later" — and you'll design the whole system around the wrong axis and rebuild it after the first review. The common mistake is treating every requirement as equally negotiable and trying to optimize all of them at once; you end up with a design that's mediocre on each and decisively good on none.

The discipline, then, is a chain, and it runs in one direction: the binding constraint forces a design, and that design costs you a capability. Naming the constraint isn't the finish line — it's what lets you predict, before you build, exactly what you'll have to give up, so you can decide on purpose whether that price is worth paying. Read the diagram below left-to-right as that chain.

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.

Constraint set conflicting Which binds first? pick one Forces a design structural Costs a capability the tradeoff
🗺️ How to read this diagram

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

You've felt this one as a user, even if you've never designed it. Type into a search box that answers instantly and you keep typing; type into one that stalls for half a second per keystroke and you give up and close the tab. For an interactive feature, slowness isn't a quality problem — it's an existence problem. That's why a latency budget, when it's real, outranks accuracy: a slightly-less-perfect answer that arrives is worth infinitely more than a perfect one the user never waited for.

The first thing to internalize is what p99 actually means, because the whole scenario hinges on it. "p99 = 300 ms" does not mean "usually about 300 ms." It means 99 out of every 100 requests must finish within 300 ms — so the number you're being graded on is the slowest common request, not the typical one. Picture a restaurant that promises "seated within 5 minutes." Averaging the wait tells you nothing useful; the promise is about the person at the back of the line on a busy night. In a request path, the person at the back is the request that misses the cache, hits the big model, and waits on a retrieval round-trip. If that path is over budget, your p99 is over budget — no matter how fast the easy requests are.

The second idea is that a latency budget is a fixed pot of milliseconds you must allocate, not a wish. Every stage on the path spends from the same 300 ms: the network round-trip, TLS and serialization, an optional retrieval hop, and finally the model's own inference time. They add up in series, so the model doesn't get 300 ms — it gets whatever is left after the unavoidable costs are subtracted. This is the crucial framing: you reserve the fixed costs first, and only then ask "does the model I want fit in the remainder?" A frontier model that takes 350 ms by itself can't fit a 300 ms total no matter what you do to the rest.

That reframing is what makes the design decisions feel forced rather than arbitrary. Once the budget is allocated, a small/distilled model, a single hop with no agentic loop, and a front cache stop being "optimizations" and become the only shapes that fit the pot. The common mistake here is optimizing the average — pointing at a high cache-hit rate and declaring victory — when the budget is defined by the miss. Below, we make the allocation concrete with a tiny model so the tradeoff is a number, not a hope.

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.

one 300 ms pot, spent in series — the model gets only what's left network 40 ms serialize/TLS 20 ms retrieval 60 ms · opt. = 120 ms reserved left for the model = 180 ms distilled 140 ms → fits ✓ frontier 350 ms → over by 170 ✗ The model gets the remainder, not the budget. Fixed costs reserve 120 ms of the 300 ms pot, leaving 180 ms — enough for a distilled model but 170 ms short of a frontier one, so latency forces the smaller model and the optional hop.
🗺️ How to read this diagram

Read the top row left to right — each box is a stage that spends from the same 300 ms pot, in series, before the model even starts.

  • The top row is the unavoidable fixed cost: network (40) + serialize/TLS (20) + the optional retrieval hop (60) = 120 ms reserved. These are spent no matter which model you pick.
  • The green bar is what's left after subtracting the fixed costs: 180 ms for the model. That remainder — not the full 300 — is the number the model has to fit inside.
  • The arrow down to the two boxes is the fit test: the green distilled model (140 ms) lands inside 180 and fits; the red frontier model (350 ms) blows past it by 170 ms and is off the table. Dropping the optional retrieval hop would hand another 60 ms back to the green bar.

In short: allocate the fixed costs first; the model must fit the leftover, which is why a hard latency budget forces a smaller model and fewer hops.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · allocate a latency budget across the request path (runs)
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}
▶ How this works

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.

  1. fixed is 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.
  2. for_model = budget_ms - reserved is the milliseconds left for inference. model_p99 is the model's own p99 latency; model_fits is just the honest comparison of the two.
  3. 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.

Budget the p99, design for the missThe cache makes your average fast, but p99 is set by the slowest common path — a cache miss. If the miss path doesn't fit the budget, no hit rate saves your p99. Allocate the budget against the miss and treat the cache as gravy on top.

Scenario 2 · $500/month hard ceiling at 1M requests advanced

This is the scenario that quietly kills promising AI features. The demo is a hit, leadership says "ship it," and three weeks after launch someone in finance forwards a $4,000 invoice with a single question mark in the subject line. The feature worked perfectly; it was just never affordable at the volume it succeeded at. A hard cost ceiling is the constraint that turns "make it good" into "make it good for less than a fraction of a cent," and the humbling part is that the math is unforgiving in a way latency never is — you can't cache your way out of arithmetic you got wrong.

The move that separates a real cost design from wishful thinking is to divide first. A $500/month ceiling over 1,000,000 requests isn't an abstract budget — it's $0.0005 per request, and that per-request number is the wall you actually design against. Do that division before anything else, because it instantly tells you which models are even candidates. If a single frontier-model summarization call costs several tenths of a cent, then sending every request to it is mathematically impossible under the ceiling, and no amount of engineering cleverness changes that. The ceiling isn't a target you aim at; it's a line the blended average of your entire traffic must land under.

The architectural answer to a per-request wall is a cascade, and it's worth understanding why a cascade is the natural shape rather than just accepting it. Real traffic is not uniform: many requests repeat (a cache serves those for free), most of the rest are easy enough for a cheap small model to handle well, and only a minority are genuinely hard enough to justify the expensive model. A cascade matches the cost of each request to its difficulty — free for repeats, cheap for the easy majority, expensive only for the escalated tail. The blended cost is a weighted average of those tiers, and the whole design succeeds or fails on whether that weighted average clears $0.0005.

Here's the trap, and it's why this scenario teaches quantification harder than any other: the escalation rate — the fraction you send to the expensive model — feels like a small knob, but it dominates the blended cost. Guess it too generously and you sail past the ceiling while believing you're fine. So you don't guess; you compute the blended cost, read the number honestly even when it says "over budget," and then tighten cache rate or escalation until it fits. The common mistake is designing the cascade, feeling clever, and never doing the arithmetic that proves it actually fits. The lab below does exactly that — and, tellingly, its first plausible-looking cascade comes out over the wall.

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.

match each request's cost to its difficulty — cheap tiers carry the volume 1M requests all incoming cache hit · 30% $0 — served free cheap model · ~59% $0.0002 each escalate · ~11% $0.004 each blended per-request cost must be ≤ $0.0005 wall 15% escalation → $0.000539 ✗ tighten to ~10% → fits ✓ The cheap tiers must carry the volume. The escalated fraction dominates the blended cost — a 15% escalation rate lands at $0.000539 and busts the $0.0005 wall, so you tighten cache or escalation until the weighted average fits.
🗺️ How to read this diagram

Read it left to right: the incoming stream splits into three tiers, then the three tiers recombine into one blended per-request number that has to clear the wall.

  • The left box is all 1,000,000 requests. Every one of them is routed to exactly one of the three tiers on its difficulty.
  • The three middle bands are the tiers, cheapest on top: green cache hits cost $0, amber cheap-model misses cost $0.0002 each, and the red escalated tail costs $0.004 — twenty times the cheap model.
  • The right box is the only number that matters: the blended (volume-weighted) cost per request. The two lines under it show the payoff — at a 15% escalation rate the average is $0.000539 and busts the wall; drop escalation to ~10% and it drops under $0.0005.

In short: a small red slice swings the whole average because it is 20× the cheap tier — so the design is won or lost on the escalation rate, which is why you compute it, not guess it.

Python · does the routing cascade fit the ceiling? (runs)
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}
▶ How this works

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.

  1. 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.
  2. total sums the cheap and expensive spend; per_req divides by all requests to get the blended cost that must beat the ceiling.
  3. 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

The first two scenarios gave you dials; this one gives you a wall — and the difference is the whole lesson. Latency and cost are constraints you optimize toward: you can be a little faster, a little cheaper, and "better" is always meaningful. Data residency is binary. The data either stays inside the boundary or it doesn't; there is no "mostly compliant." A latency miss is a slow response and an apologetic Slack message. A residency miss is a regulated-data breach, a legal event, possibly a fine and a headline. Once you feel that asymmetry, the design stops being a negotiation and becomes a filter.

What a binary wall does — and what makes this an expert-level scenario — is remove options from the board entirely, before you evaluate them on merit. The most capable models on the market are closed APIs; using them means the request payload travels to a vendor's servers. Under a no-egress rule, that single fact disqualifies the frontier API completely, no matter how much better it is at the task. This is a genuinely different mental motion from optimization: you're not comparing candidates and picking the best, you're crossing out every candidate that touches the network and picking the best of what remains. The forced move is a self-hosted open-weight model on infrastructure you control, and everything downstream — owning GPUs, running a serving stack, quantizing to fit the hardware, carrying an on-call — follows mechanically from that one non-negotiable decision.

Now the part that trips up even strong engineers, and the reason this scenario is worth its own section: a binding constraint constrains the whole system, not just the component you were thinking about. When teams hear "no data leaves the network," they picture the model call and stop there. But residency is a property of the entire request path — and request data flows through far more than inference. It flows into logs, into distributed traces, into error payloads sent to a crash reporter, into an APM dashboard, into backups. Every one of those is an egress point. Self-host the model, then pipe your prompts to a hosted logging SaaS for "observability," and you've defeated the constraint through the plumbing — the data left through a side door you forgot you'd built.

The common mistake, then, isn't choosing the wrong model; it's declaring the constraint satisfied the moment inference is in-house, while telemetry quietly ships the very data you self-hosted to protect. The design isn't finished until you can trace every byte of request data and confirm none of it reaches a network you don't control. The comparison table and the worked design below make the forced tradeoff — and the leak you have to hunt for — explicit.

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.

🔒 inside the corporate network boundary outside — vendor networks request self-hosted model in-house ✓ response logs traces / APM error payloads crash reporter vendor SaaS data leaked ✗ Residency is a whole-path property. Self-hosting inference keeps the model call in-house — but logs, traces, and error payloads are egress points too; pipe any of them to a hosted observability SaaS and the data leaves through a side door you forgot you built.
🗺️ How to read this diagram

The dashed blue box is the network boundary — everything the no-egress rule says must stay inside. Scan the top row first (the path teams think about), then the bottom row (the path they forget).

  • The top row is the request → green self-hosted model → response. This is the part teams fix: inference never leaves the boundary, so it looks compliant.
  • The bottom amber boxes — logs, traces/APM, error payloads — all see the same request data. They are egress points too, not just the model.
  • The amber arrows lead out of the dashed box to the red vendor SaaS: the moment any of those telemetry streams ships to a hosted service, the regulated data has left the network. Self-hosting the model bought you nothing.

In short: a binding constraint constrains the whole system — the design isn't done until every byte of request data, including telemetry, is proven to stay inside the boundary.

AxisClosed API (banned here)Self-hosted open (forced)
Data residencyleaves the network ✗stays in-house ✓
Capabilityfrontiertrails on the hardest reasoning
Ops burdennoneyou own GPUs + serving + on-call
Cost shapeper-token, elasticfixed 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.

The leak is usually in the plumbing, not the modelTeams self-host the model, then pipe prompts to a cloud logging service, send stack traces with payloads to an error tracker, or trace to a hosted APM. The data left the network through the side door. Residency is a property of the entire request path — model, logs, traces, backups — not just where inference runs.

Scenario 4 · Must work offline / air-gapped expert

If residency was a wall, air-gap is the same wall built one layer higher — and the extra layer changes everything. "No data leaves the network" still assumes a network exists; you just keep the data inside it. Air-gapped removes the network itself. Picture a system on a submarine, in a classified facility, or on a field laptop in a place with no signal: there is no API to call, no web search to run, no pip install to reach for when a dependency is missing, no license server to phone. Everything the system will ever need at runtime must already be on the box before it goes dark. Engineers who've only ever built connected systems consistently underestimate how many quiet network dependencies they've been leaning on — and each one becomes a crash the moment the cable is pulled.

The reframing that makes air-gap tractable is this: the deliverable stops being a running service and becomes a self-contained artifact. In a normal deployment, "the app" is code that reaches out to models, indexes, and package registries as it runs. Air-gapped, "the app" is a single bundle that contains all of those — quantized model weights, the tokenizer, a pre-built retrieval index, every pinned dependency — vendored in ahead of time. Anything that would normally be fetched on demand has to be either baked in or deleted from the design. A "tool" the agent can call is only allowed if it's a local function: a file reader, a calculator, a local SQLite query. The instant a tool would hit a URL, it's off the table.

That constraint reshapes retrieval in a way worth naming, because it's the least obvious casualty. RAG normally implies a live index you can query and update. Air-gapped, the index becomes a frozen snapshot shipped on disk — built offline, carried across the gap, and stale until a human physically re-images the machine. There is no "refresh from source"; freshness is traded away wholesale for autonomy. Similarly, model quality is capped by what fits the on-box hardware after quantization, which on a tight machine may mean accepting a 4-bit model and its accuracy hit. You are choosing a smaller, older, frozen system on purpose because the alternative — needing the network — simply doesn't exist in this world.

The common mistake is assuming "offline" is just "online minus the API calls," and shipping something that looks self-contained but isn't: a library that lazy-downloads a model file on first use, a tokenizer that fetches vocab from a CDN, a "local" tool that quietly resolves a hostname. Those hidden assumptions don't error politely — they hang or crash in the field where nobody can debug them. The only honest test is to pull the network physically, not just firewall it, and see if the whole thing still answers. The build checklist below is the discipline that forces every runtime dependency into the bundle before it's too late to add one.

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.

the deliverable is one self-contained artifact — nothing fetched at runtime 📦 bundled on the box, before it goes dark quantized weights 4-bit if VRAM tight tokenizer vendored, no CDN pre-built index frozen snapshot local tools only file · calc · SQLite all dependencies pinned + vendored deleted from the design hosted model API ✗ live web search ✗ pip install at runtime ✗ The artifact is the product. Everything the system will ever need at runtime — weights, tokenizer, index, local tools, pinned deps — is baked into one bundle before it goes offline; anything that would reach the network on demand is deleted from the design, not deferred.
🗺️ How to read this diagram

Two columns split by the dividing line: on the left, everything that ships inside the bundle; on the right, everything that gets crossed off because it needs a network that no longer exists.

  • The green box on the left is the artifact — the single deliverable. Every green tile inside it (weights, tokenizer, pre-built index, local-only tools, pinned dependencies) is vendored in ahead of time.
  • The 'local tools only' tile is the subtle one: a tool is allowed only if it's a local function — a file read, a calculator, a SQLite query. The instant it would hit a URL it moves to the right column.
  • The red boxes on the right — hosted model API, live web search, runtime pip install — are the network conveniences that get deleted, not postponed. If it isn't in the bundle before the box goes dark, it doesn't exist at runtime.

In short: air-gap turns "the app" from a service that reaches out into a frozen bundle that contains everything — trading freshness for the ability to run with the cable pulled.

Air-gap build checklist

  1. Pick a model that fits the hardware quantized — 4-bit if VRAM is tight (accept the quality hit).
  2. Vendor the weights + tokenizer into the image; no download at runtime.
  3. Replace every server/tool call with a local equivalent, or drop the feature.
  4. Pre-build the retrieval index offline and ship it on disk; refresh only on a manual re-image.
  5. Pin and vendor all dependencies — assume pip install will 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

Every engineer has watched a service die at exactly the moment it mattered most. The launch goes viral, the flash sale hits, the news picks up the product — the thing you wanted to happen happens — and the system falls over precisely because it succeeded. What makes this a tech-lead scenario rather than an engineering one is that the naïve fixes are both wrong in instructive ways. Provision for the 10× peak permanently and you burn money on idle GPUs 99% of the time. Provision for the average and you collapse at the peak. And unlike a stateless web tier you can autoscale in seconds, GPU capacity is slow and expensive to add, so "just autoscale" doesn't save you when the spike arrives faster than replicas can warm.

The mental shift that resolves it: stop trying to pick a single point on the capacity curve, and instead design a system that degrades in defined stages. The product owner already told you the shape of the answer — they'll accept degraded service but not an outage. That single sentence is permission to trade quality for availability, and it turns an impossible sizing problem into a tractable sequencing problem: not "how much capacity?" but "in what order do I give things up as load climbs?"

The order is not arbitrary — it runs from least painful to most, and each rung buys time for the next to be a last resort rather than a first reflex:

The reason this is a design and not a reflex is that every one of those trades is pre-decided. When the system is drowning at 2 a.m., you do not want a human choosing which corners to cut — you want the system to already know, because you decided in calm daylight and load-tested it. The common mistake is drawing only the happy path and treating the degradation ladder as something you'll "figure out under pressure"; the paths you never rehearse rot silently — the small-model route starts 500ing, the queue OOMs, load-shedding drops the wrong tier — and you discover it during the incident. The diagram below lays out the ladder in order so the sacrifice sequence is explicit and defensible in the post-incident review.

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.

Load rises normal Queue(add latency) buffer bursts Autoscaleto ceiling more replicas Degrade quality smaller/cheaper Shed low-priority last resort
🗺️ How to read this diagram

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.

A tech lead designs the failure, not just the successAnyone can draw the happy path. The staff-level skill is deciding in advance what the system sacrifices, and in what order, when it can't serve everyone perfectly. Queue → autoscale → degrade → shed is a pre-committed policy so the system makes the right call under load — and so you can defend it in the post-incident review.

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.

📋 Grade your design — defensible tradeoffs
DimensionMeets barAbove bar
Named the binding constraintidentifies which constraint forces the designshows why it binds before others and what order the rest fall in
Made the tradeoff explicitstates what is sacrificed for the constraintstates it in the design's own terms and where it hurts most
Quantified itputs a rough number on the cost/latency/gapmodels it (budget/cost/break-even) and reads the number honestly, even when it fails
Considered failure modesnames the main way the design breakscovers the non-obvious ones (miss path, telemetry leak, hidden network, untested degradation)
Defensiblecould justify the call to a skepticpre-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.

✓ Knowledge check

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
Check the miss / worst-case path against the p99 budget, not the average. p99 is set by the slowest common path — a cache miss going to the frontier model plus a retrieval round-trip. Averages hide that: a high cache hit rate makes the mean look great while the 99th percentile still blows the budget on cold queries. The binding constraint is latency, so you size the model and drop hops until the miss path fits 300 ms; the cache is then a bonus on top.
✓ Knowledge check

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
No — the constraint is violated through the plumbing. Data residency is a property of the entire request path (model, logs, traces, error payloads, backups), not just where inference runs. The lesson: a binding constraint constrains the whole system, not the one component you were thinking about. When residency binds, every egress point — including telemetry — has to stay inside the boundary, or the self-hosting bought you nothing.

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.py or routing_cost.py to 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.

Exercise 1 · Name the binding constraintBeginner

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.

Exercise 2 · A 300 ms p99 latency budgetIntermediate

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):

StageBudget
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.

Exercise 3 · $500/month hard ceiling at 1M requestsAdvanced

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.

  1. 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.
  2. Tier the models: route easy/short queries to a cheap small model, escalate only the hard ones. Most traffic is easy.
  3. 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.

Exercise 4 · No data may leave our networkExpert

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:

  1. 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.
  2. 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?’

Exercise 5 · Must work offline / air-gappedProfessional

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.

  1. Immutable artifact: ship a single versioned image/bundle containing weights + code + deps. What ran in staging is byte-identical to prod.
  2. 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.
  3. Local observability: metrics and logs stay on-box with a local dashboard; ops reads them without egress.
  4. 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.

Exercise 6 · A 10x traffic spike is comingIndustry scenario

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.

  1. 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.
  2. Horizontally scale the stateless tiers: API and retrieval scale out easily; provision extra replicas ahead of time, not reactively.
  3. 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’.
  4. 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.
  5. Load-test to 10x now: find the breaking component this week, in staging, not during the launch.
TierLoad response
NormalFull RAG
HighCache-first, small model on miss
OverloadCached 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.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in