Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

AI System Design

Design ChatGPT

Step 1 / 9

Learn AI system design by designing ChatGPT itself — a chat product serving hundreds of millions of weekly users and billions of prompts a day.

The numbers to beat~800Mweekly users2.5Bprompts / dayany cellserves any user

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

"Design ChatGPT" — what actually makes it hard?

It’s the most-asked AI system design question for a reason. On paper it’s "a form that calls a model." In production it’s ~800M weekly users, on the order of 2.5 billion prompts a day, and every single reply is generated token-by-token on scarce, expensive GPUs. Three things make it hard: conversations are stateful but models are stateless, GPU capacity is the bottleneck for everything, and users judge you on how fast the first token appears.

User2.5B prompts/day
New in this step: User.

Design it in layers: a stateless serving path anyone can scale, durable conversation state on the side, a context builder that reconciles the two every turn, and an inference layer that squeezes every token out of the GPUs — with a deliberate degradation plan for the day traffic outruns capacity.

What the new pieces do

Userclient
One of ~800M weekly users. Sends a message in an ongoing conversation and expects the reply to start appearing in about a second.

Step 1 · The skeleton

Edge in front, stateless cells behind

Hundreds of millions of users worldwide, hitting you in bursts. Before any AI happens you need boring excellence: authentication, rate limits, and routing to a nearby region. And a fleet of chat servers that can scale horizontally without caring which user lands where.

Edge / Gatewayauth · quotasChat Servicestateless cell
New in this step: Edge / Gateway, Chat Service.

A user’s next message arrives. Which server should handle it?

  1. Sticky sessions pin users to machines: hot spots, painful deploys, and a lost conversation every time a server dies. State on the serving path is the enemy at this scale.

  2. Billions of conversations would need billions of homes, most idle. You’d be building a distributed state machine you don’t need.

  3. The chat service is stateless: state lives in a store, and every instance can serve every user. That single decision is what makes the fleet trivially scalable.

A global edge/gateway terminates TLS, authenticates, enforces per-user quotas, and routes to the nearest region. Behind it, the Chat Service is a fleet of identical, stateless instances — any one can serve any request, so scaling is "add more."

Why this piece earns its place

Statelessness makes a cell safe to lose, but a chat turn is not a request — it is a connection held open while an answer is generated, and that is what makes the property awkward in practice. A cell holds nothing durable and still holds work in flight, so a release needs a drain mode: refuse new turns, finish the ones already streaming, then exit. The reconnect path will regenerate whatever you cut, so no conversation is at risk, but regenerating is not free — it re-runs the context builder and pays the prefill again on the GPU fleet. Restart a large share of the fleet at once and a routine release becomes a burst of duplicated inference at the moment you have the fewest cells to absorb it, which is an argument for staggering the roll and capping how many drain together. Those same long-lived connections also mislead the load balancer: request rate and CPU both read a cell nursing slow streams as idle, and the quantity worth balancing on is in-flight generations. At one region and a few thousand users you would skip all of it. What the extra hop buys is that the crash path and the release path become the same path — so the one you exercise on every deploy is the one that has to work when a rack dies.

  • ~800Mweekly users
  • 2.5Bprompts / day
  • any cellserves any user

What the new pieces do

Edge / Gatewayedge
Global anycast edge: terminates TLS, authenticates, enforces per-user rate limits and quotas, and routes to the nearest healthy region.
Chat Servicebackend
The per-request orchestrator. Loads history, builds the prompt, calls inference, streams tokens back. Holds no state itself, so any instance can serve any user.

Step 2 · Remember everything

The conversation store

Conversations are the product: users scroll years of history, resume any thread, and expect nothing to vanish. That’s billions of conversations, most cold, some scorching hot. Where does this state live?

Edge / Gatewayauth · quotasChat Servicestateless cellConversation Storesharded by chat
New in this step: Conversation Store.

How do you store billions of conversations?

  1. A single write master melts under this write volume. Nothing about this access pattern needs global joins — it’s all "fetch this one conversation."

  2. Every read and write says which conversation it belongs to, so conversation id is the natural shard key. Recent turns stay hot; year-old threads tier down to cheap storage.

  3. A crash loses a day of conversations, and step 1 just made the servers stateless on purpose. Durable state belongs in a store, not a process.

A Conversation Store sharded by conversation id — the access pattern is always "append a turn / read recent turns of one thread," which shards perfectly. Recent messages sit on hot storage; old threads tier to cold object storage nobody pays premium prices to keep.

Why this piece earns its place

Two decisions here are cheap now and expensive later. The first is that an append has to survive being repeated. The retry path regenerates a turn on a different cell, so the same message can reach the store twice, and an auto-incrementing turn number turns that into a user watching their own question appear in their history twice. Derive the row id from what the caller already holds — the conversation id plus a client-side message id — and the duplicate write lands as a no-op. It is also the argument for a row per turn rather than one growing document per thread: appending to a document makes every write a read-modify-write, and there the same duplicate is a lost update instead of a no-op. The second decision is shape. A list is the obvious model, but users edit a message and regenerate a reply, and the moment they do, a conversation is a tree with one currently-selected path through it. Retrofitting that onto a list is a migration across billions of rows; carrying a parent_id from the start costs one column. Then watch where tiering hurts: resuming a two-year-old thread puts a cold-storage read on the critical path before any prefill begins, so it surfaces in dashboards as slow time-to-first-token and sends you hunting through the GPU fleet for a problem that lives in object storage.

What the new pieces do

Conversation Storestore
Durable home of every conversation, sharded by conversation id. Recent turns on hot storage, old conversations tiered to cheap cold storage.

Step 3 · The stateless model

Rebuild the context every turn

Here’s the trap most candidates fall into: the model does not remember your conversation. Every API call starts from nothing. So how does turn 47 know what happened in turns 1–46?

Chat Servicestateless cellContext Buildertoken budget
New in this step: Context Builder.

How does the model "remember" earlier turns?

  1. Exactly. Each turn, the context builder reassembles the model’s entire input: system prompt, user memory, and as much recent history as fits. "Memory" is an illusion built by the serving system.

  2. Inference is stateless — weights are frozen and no activations persist between requests. If the history isn’t in the prompt, it never happened.

  3. Training per user would be ruinously expensive, days stale, and a privacy minefield. Context assembly, not weight updates, is how chat products remember.

The Context Builder reconstructs the prompt every single turn: system prompt + long-term user memory + recent turns, packed newest-first until the token budget is spent. Older history gets truncated or summarized. The model is stateless; the system supplies the memory.

Why this piece earns its place

The thing worth saying out loud is that this component’s output is a cache key. Prefix reuse downstream depends on this turn’s prompt beginning with the same bytes as last turn’s, and that demands a determinism ordinary request code never has. A timestamp in the system prompt, memory facts emitted in whatever order a set happened to iterate, an experiment flag rewording an instruction: any one changes byte zero, and the prefix is recomputed for that user every turn, forever. Nothing errors and no latency alarm fires; the bill just goes up. The largest breaker, though, is this step’s own policy — when the budget pushes the oldest turn out, everything behind it shifts and the shared prefix ends where the window now starts. So trim in blocks rather than a turn at a time and the break lands once every few turns instead of on every one. The layering above is the cache-safe order for the same reason: the system prompt changes on release, memory occasionally, history every turn, so what moves most sits last and invalidates least. Summarization is that trade plus a write — recomputing a summary per turn would add a model call to every request, so summaries are computed once and stored beside the turns they stand in for, which quietly makes this component a writer and gives you something to invalidate when a user edits an old message.

  • every turnprompt rebuilt from scratch
  • token budgethard cap per request
  • summarizewhen history outgrows it

What the new pieces do

Context Builderservice
Rebuilds the model’s entire input every turn: system prompt + user memory + as much recent history as the token budget allows.

Step 4 · Not every prompt deserves a frontier model

The model router

"What’s 2+2?" and "prove this theorem" cost wildly different amounts to answer well — but naive systems send both to the biggest model. At billions of prompts a day, that’s setting money on fire. How do you serve both cheaply and well?

Context Buildertoken budgetModel Routersmall vs frontier
New in this step: Model Router.

Billions of daily prompts, models from cheap-and-small to frontier. Who decides which model answers?

  1. Most prompts are short, easy, conversational. Answering them with the frontier model multiplies your GPU bill for quality users can’t perceive.

  2. A tiny, fast classifier routes easy traffic to small models and hard reasoning to the frontier model. Done well, it cuts serving cost by multiples with no visible quality drop.

  3. A power-user feature, not a strategy — most users won’t choose, and the default still has to be routed. The system needs its own judgment.

A Model Router scores each prompt — length, topic, reasoning depth, user tier — and dispatches it to the cheapest model that clears the quality bar, escalating to the frontier model only when needed. Routing thresholds are tuned continuously against eval data.

Why this piece earns its place

Routing per prompt sounds obviously right until you notice it is per prompt, inside a conversation that already has cached state attached to one model. KV state is model-specific, so sending turn 13 somewhere different from turn 12 means the whole history is prefilled again from nothing — a route that looks cheaper per token can cost more than staying put. That is why this component has to be cache-aware: the question is not only which model is capable, it is what leaving costs. Sticky-per-conversation routing with a one-way escalation upward is usually the rule that survives contact with production. Two operational notes. The classifier runs ahead of prefill, so its latency lands in time-to-first-token in full and it has to be small enough that the saving is not handed back at the front door. And record the model actually chosen with the turn, not the tier that was asked for — without it you cannot replay a bad answer, and every quality complaint becomes an argument about which model wrote it.

What the new pieces do

Model Routerservice
Classifies each prompt and routes it: easy traffic to a small cheap model, hard reasoning to the frontier model. The single biggest cost lever.

Step 5 · Stop paying for the same tokens

Prompt / prefix caching

Look at what the model actually receives each turn: the same system prompt, the same instructions, the same conversation history as last turn — plus one new message. Recomputing attention over that unchanged prefix, every turn, for every user, is an enormous, pure waste.

Chat Servicestateless cellContext Buildertoken budgetModel Routercache-awarePrompt Cacheprefix reuse
New in this step: Prompt Cache.

Most of each request’s prompt is identical to the previous turn. What do you do with that fact?

  1. Prefill (processing the input) is a huge share of inference compute. Ignoring redundancy here leaves massive capacity on the table.

  2. Response caches barely hit in open-ended chat — nearly every conversation is unique. The redundancy worth exploiting is in the prefix, not the answer.

  3. Prefix/KV caching stores the attention state of unchanged prompt prefixes, so the GPU only processes the new tokens. Cheaper prefill AND a much faster first token.

A Prompt Cache keeps the computed KV state for common prefixes — system prompts shared by everyone, and each conversation’s unchanged history. On a hit, prefill work shrinks to just the new tokens, cutting cost and time-to-first-token dramatically.

Why this piece earns its place

The catch is that a hit is not free to reach. KV blocks are cheapest to reuse on the accelerator that computed them; anywhere else — host memory, a shared KV tier — reusing them means moving them across a link before prefill can skip anything. That is still far cheaper than recomputing the prefix, but it is not nothing, so this cache quietly turns a uniform fleet into one with soft affinity: whatever places a turn now prefers the machine already holding that conversation’s blocks, and has to decide per request whether the preference is worth queueing for a busy slot. Lean on locality and the chattiest threads pile onto the same machines; ignore it and you pay a transfer every turn. The second tension is memory. Blocks held for a future turn occupy space a running generation would otherwise use, so every prefix retained is a request you cannot admit. The shared system prompt is easy — everyone hits it, so it stays pinned. One conversation’s history is a bet that its user replies before that space is worth more to somebody else, which makes eviction something to fit against the gap between turns your telemetry already measures, not a constant somebody picked once. If the traffic is mostly one-shot prompts rather than long threads, pin the shared prefix and skip per-conversation caching entirely.

  • same prefixturn after turn
  • cache hitprocess only new tokens
  • TTFTdrops with prefill

What the new pieces do

Prompt Cachestore
Caches the computed (KV) state of shared prefixes — system prompts and unchanged history — so prefill work isn’t redone every turn.

Step 6 · The GPU fleet

Continuous batching, or bankruptcy

A GPU running one user’s request at a time idles most of its silicon — decode is memory-bound, not compute-bound. At this scale, serving requests one-by-one wouldn’t just be slow, it would be financially impossible. How does one GPU serve dozens of users at once?

Batch Schedulercontinuous batchingGPU FleetKV cache · parallel
New in this step: Batch Scheduler, GPU Fleet.

How does a single GPU serve many users’ generations at once?

  1. That’s millions of GPUs. Decode barely uses the GPU’s compute per request; the whole game is sharing each GPU across many requests.

  2. Continuous batching: every decode step advances dozens of interleaved requests, and the scheduler slots new arrivals into seats as they free up — no waiting for the batch to drain.

  3. Swapping means reloading each request’s KV state — exactly the expensive thing to avoid. Batching runs them simultaneously, not in turns.

The Batch Scheduler packs requests onto the GPU Fleet with continuous batching: each forward pass advances every in-flight request by one token, and newly-arrived requests join the moment a slot frees. The binding constraint is KV-cache memory per GPU — that, not compute, decides how many users fit. Admission control keeps queue depth bounded instead of letting latency grow without limit.

Why this piece earns its place

Admission here is a bet, and that is what makes it unlike every other queue you have built. A web request’s cost is known when it arrives; a generation’s is not, because its KV footprint grows with every token and nobody knows how long the answer runs until it stops. So the scheduler admits on an estimate and must be able to preempt — evicting an in-flight request’s state and recomputing or swapping it back later — when the batch outgrows the memory it was admitted against. Build that early, because the alternative is abandoning a half-written reply. The other thing to know is who pays. A new arrival’s prefill is one large compute burst that stalls the decode step of everything already running, so a user who pastes a long prompt degrades other people’s inter-token latency rather than their own. Per-request dashboards look fine throughout, because the victim and the cause are different requests. Chunked prefill exists for exactly that.

What the new pieces do

Batch Schedulerservice
Admission control + batching brain. Packs many users’ requests onto each GPU, joins new requests mid-flight, and bounds queue depth.
GPU Fleetgpu
Racks of accelerators running the models. Throughput comes from batching; the scarce resource is KV-cache memory per GPU.

Back of the envelope

continuous batching
join and leave the batch mid-flight, no drain
KV-cache memory
the real capacity limit per GPU
admission control
bound the queue; reject/degrade over timing out
tensor parallel
shard big models across GPUs in a node

Step 7 · Feel fast

Stream tokens, watch TTFT

A good answer might take 20 seconds to fully generate. Users will not stare at a spinner for 20 seconds — but they’ll happily read along as the answer appears. What does the response path look like, and what number do you actually optimize?

Edge / GatewayChat ServiceConversation StorePrompt CacheBatch SchedulerGPU FleetTelemetry / Evals
New in this step: Telemetry / Evals. · swipe to pan the diagram

Generation takes ~20s end-to-end. How do you keep it from feeling slow?

  1. Server-sent events / streaming turns 20 dead seconds into a live, readable answer. Perceived latency collapses to the time the FIRST token takes.

  2. A progress bar is still a wait. Streaming gives users the actual product — words — hundreds of times sooner.

  3. Serving a wrong-then-corrected answer erodes trust and doubles cost. Stream the real answer; speed comes from TTFT, not tricks.

Tokens stream back through the chat service to the client as they’re decoded (SSE/WebSocket), the turn is appended to the conversation store, and every request logs TTFT (time to first token), tokens/sec, cost and quality signals to telemetry — the data that tunes the router, the cache and capacity planning. The headline metric is TTFT: it’s what "fast" means in chat.

Why this piece earns its place

Two things about streaming only appear once real users are on it. The first is cancellation. Someone closes the tab, and unless that disconnect travels back through the chat cell to the scheduler, the GPU keeps generating an answer nobody will read — capacity spent on abandoned work that never shows up as an error anywhere. Treat a client disconnect as a first-class signal that frees a batch slot, and decide deliberately whether a partly streamed reply is persisted; if it is not, a user who scrolls away mid-answer returns to a thread holding their question and no reply. The second is measurement. TTFT collected on the server is not what anyone experienced, because proxies buffer streamed responses and a buffer hides the entire problem — instrument it in the client. And the number is bimodal, not noisy: a prefix hit and a prefix miss are two populations with different physics, so one average sits between them and describes neither. Segment by cache outcome and by model, or the metric that defines the product moves for reasons you cannot attribute.

  • TTFTthe number users feel
  • tokens/sreading speed or better
  • every turnlogged for evals

What the new pieces do

Telemetry / Evalsbus
Every request’s latency, token counts, cost and quality signals — the feedback loop for capacity planning, routing thresholds and model evals.

The payoff

You designed ChatGPT

From "a form that calls a model" to a planet-scale chat system: stateless cells behind a global edge, conversations sharded by id, context rebuilt every turn under a token budget, a router picking the cheapest capable model, prefix caching killing redundant prefill, continuous batching squeezing the GPUs, and tokens streaming back the moment they exist.

UserEdge / GatewayChat ServiceConversation StoreContext BuilderModel RouterPrompt CacheBatch SchedulerGPU FleetTelemetry / Evals
The finished design, end to end. · swipe to pan the diagram

Now spike the traffic and watch the degradation playbook: queue bounds, silent routing downgrades, cache absorption, and free-tier shedding — the difference between a graceful brownout and a global outage.

Everything you assembled, in order

  • Stateless cells — any server serves any user; state lives in stores
  • Conversation store — sharded by conversation id, hot/cold tiered
  • Context builder — model is stateless — rebuild the prompt every turn
  • Model router — cheapest capable model; the #1 cost lever
  • Prompt cache — reuse KV state of unchanged prefixes
  • Continuous batching — dozens of users per GPU, join mid-flight
  • KV-cache memory — the real per-GPU capacity limit
  • Stream + TTFT — perceived speed = time to first token

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. A chat cell dies mid-stream, halfway through generating a reply. What does the user experience?

    A cut-off stream — because the cell held no state, the retry path is trivial: the client reconnects, a different (also stateless) cell picks up the request, re-runs the context builder from the conversation store, and regenerates. The user might see a stutter or a restarted reply, but the conversation itself was never at risk, because it never lived on the cell that crashed.

  2. Why does the context builder pack history newest-first instead of chronologically, and what actually gets cut when the budget runs out?

    Recent turns are the ones most likely to matter to the next reply, so they’re protected first; what gets trimmed or summarized is the OLDEST history, not the newest. A pure chronological pack would silently truncate the most recent (most relevant) turns first if the budget ran tight — exactly backwards from what a user expects "remembering the conversation" to mean.

  3. Why can’t the router’s classification thresholds just be tuned once and left alone?

    Because the cost/quality tradeoff drifts under you — model updates change what "easy" looks like, user behavior shifts, and a threshold that was safe last month can quietly start sending hard prompts to the small model (visible quality drop) or easy prompts to the frontier model (invisible cost bleed). It’s tuned continuously against eval data because the ground truth it’s approximating moves.

  4. Prefix caching and continuous batching both make the GPU fleet cheaper — why are they two separate mechanisms instead of one?

    They attack different phases of inference. Prefix caching skips redundant PREFILL work (processing a prompt that’s mostly identical to last turn); continuous batching packs many requests’ DECODE steps (token-by-token generation) onto one forward pass. A cache hit doesn’t help decode throughput, and better batching doesn’t reduce prefill cost — you need both because the waste lives in different places.

  5. How would multi-modal prompts — a user pastes a screenshot — change this architecture?

    The context builder now has to budget vision tokens (often expensive relative to text) alongside conversation history, the router needs a model tier that actually supports vision (not every small model does), and the GPU fleet’s KV-cache math changes because image tokens inflate the prefill cost per request — the skeleton holds, but every budget in it gets tighter.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Why can any chat server handle any user’s next message?

    State lives in the conversation store, keyed by conversation id; the serving fleet holds nothing, so it scales horizontally and fails safely.

  2. How does the model "remember" turn 1 when answering turn 47?

    Inference is stateless. Every request’s prompt is rebuilt from system prompt + memory + trimmed history under a token budget.

  3. The single biggest lever on serving cost is…

    Cost is dominated by which model runs. Routing even half the traffic to a 10×-cheaper model roughly halves the GPU bill.

  4. Continuous batching means…

    Each forward pass advances all in-flight requests one token, and new arrivals take freed slots immediately — no drain, no wait.

  5. A conversation-store shard goes down. What happens to users on OTHER shards?

    Sharding on the access pattern means a shard failure only touches the conversations that hash to it — the rest of the fleet keeps serving normally.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Chat: send a message in an ongoing conversation and get a reply that starts appearing in about a second.
  • Remember: store and resume any conversation — years of history, nothing vanishes.
  • Rebuild context: assemble the model’s input every turn under a token budget.
  • Route models: answer each prompt with the cheapest model that clears the quality bar.
  • Stream & shed: emit tokens as they’re generated, and shed load in order when capacity runs out.

The qualities that shape everything

Each one names the mechanism that buys it.

The serving fleet scales linearly
Stateless chat cells behind a global edge — state lives in a store, so any instance serves any user and scaling is “add more.”
Store billions of conversations
A conversation store sharded by conversation id (single-shard reads/writes), with recent turns hot and old threads tiered to cold storage.
The model “remembers” though it’s stateless
A context builder rebuilds the entire prompt every turn — system prompt + memory + as much recent history as the token budget allows.
Serving cost stays sane at billions of prompts/day
A model router scores each prompt and sends it to the cheapest capable model, escalating to the frontier model only when needed — the #1 cost lever.
Cheaper prefill and faster first token
A prompt cache keeps the KV state of shared prefixes so prefill shrinks to just the new tokens on a hit.
Many users share each GPU
Continuous batching packs requests onto the GPU fleet joining mid-flight; KV-cache memory is the real capacity limit and admission control bounds the queue.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Stateless chat cells over sticky sessions on the same server

Pinning a user to the machine that “remembers” their conversation creates hot spots, painful deploys and a lost thread every time a server dies. Keeping state in a store lets any cell serve any user and scale linearly.

Rebuilding context every turn over the model keeping internal state

Inference is stateless — weights are frozen and nothing persists between calls, so if the history isn’t in the prompt it never happened. The system re-sends memory + trimmed history each turn; fine-tuning per user would be ruinous, stale and a privacy minefield.

A model router over always using the frontier model

Most prompts are short and easy; answering them with the biggest model multiplies the GPU bill for quality users can’t perceive. Routing even half the traffic to a 10×-cheaper model roughly halves serving cost.

Prefix (KV) caching over caching final answers

Response caches barely hit in open-ended chat — nearly every conversation is unique. The redundancy worth exploiting is the unchanged prefix, whose KV state can be reused to cut prefill and time-to-first-token.

Continuous batching over one GPU per active request

Per-request decode is memory-bound and wastes most of the silicon, and a GPU per request would need millions of them. Advancing dozens of interleaved requests per forward pass, joining new ones as slots free, is what makes tokens-per-dollar work.

The answer, out loud

What a strong answer to “Design ChatGPT” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Name what actually makes this hard

    Before I draw anything I want to say what makes this hard, because “a form that calls a model” is the trap. Three things. Conversations are stateful but the model is not — every inference call starts from nothing. GPU capacity is the bottleneck for everything downstream, and I can’t conjure more of it in the middle of a spike. And users judge this product on how fast the first token appears, not on when the whole answer finishes. At roughly eight hundred million weekly users and on the order of two and a half billion prompts a day, each of those becomes a systems problem. So I’d build it in layers: a serving path that scales, state on the side, something that reconciles the two every turn, and an inference layer I squeeze hard.

  2. 3–8 min

    A stateless spine behind a global edge

    I’d start with the boring part, because it’s load-bearing. A global edge terminates TLS, authenticates, enforces per-user quotas and hands off to a nearby region. Behind it I want chat cells that hold no conversation state at all, so any cell can serve any user and scaling is just “add more.” The alternative is pinning a user to the machine that remembers them, and I’d rather not: that gives me hot spots, a lost thread every time a box dies, and deploys I’m afraid of. Statelessness is what makes everything after this cheap.

    Built in step 1: Edge in front, stateless cells behind
  3. 8–14 min

    Where the conversations live

    State has to go somewhere, so: a conversation store sharded by conversation id. I pick that key because every access is “append a turn to this thread” or “read the recent turns of this thread” — nothing in this product joins across conversations, so I get single-shard reads and writes and no hot master. Recent turns stay on hot storage and old threads tier down to cold object storage, because most of those billions of conversations are never opened again. The side effect I’d point at is blast radius: if a shard goes dark, only the conversations that hash to it are affected and everyone else keeps chatting.

    Built in step 2: The conversation store
  4. 14–20 min

    The model doesn’t remember — the system does

    Now the part I’d be explicit about, because it’s where this question is usually failed. The model does not remember turn 1 when it answers turn 47. Inference is stateless — the weights are frozen and nothing persists between calls. So a context builder reassembles the entire input every single turn: system prompt, long-term user memory, then as much recent history as the token budget allows, newest first, summarizing or cutting the oldest when it won’t fit. Fine-tuning per user is the tempting alternative and it’s a bad one: ruinously expensive, days stale, and a privacy minefield. Memory here is an illusion the serving system rebuilds, one request at a time.

    Built in step 3: Rebuild the context every turn
  5. 20–26 min

    Route before you spend

    Then cost — at this volume, cost is the design. “What’s 2+2” and “prove this theorem” don’t deserve the same model, but a naive system sends both to the biggest one. So I’d put a model router in front: score each prompt on length, topic, reasoning depth and user tier, send it to the cheapest model that clears the quality bar, and escalate to the frontier model only when it has to. This is the single biggest lever I have — routing even half the traffic to a model ten times cheaper roughly halves the serving bill, and nothing else on the diagram comes close. I’d tune those thresholds continuously against eval data rather than setting them once.

    Built in step 4: The model router
  6. 26–31 min

    Stop paying for the same prefix

    Now the inference side, and I’d split it by phase. Prefill first: most of what the model receives this turn is identical to last turn — same system prompt, same history, one new message — so a prompt cache keeps the computed KV state of those shared prefixes and on a hit the GPU only processes the new tokens. That’s cheaper prefill and a much faster first token from the same fleet. I’d be clear this is not a response cache: in open-ended chat nearly every conversation is unique, so caching answers barely hits. The reusable thing is the prefix.

    Built in step 5: Prompt / prefix caching
  7. 31–35 min

    Many users per GPU

    Decode is the other half. One request at a time idles most of a GPU, because decode is memory-bound rather than compute-bound, so a batch scheduler runs continuous batching: each forward pass advances every in-flight request by a token, and new arrivals take seats the moment they free up. The constraint that actually binds is KV-cache memory per GPU — that, not compute, decides how many users fit — and admission control bounds queue depth instead of letting latency climb without limit.

    Built in step 6: Continuous batching, or bankruptcy
  8. 35–39 min

    Stream it, then say what “fast” means

    The last piece is the response path. Tokens stream back through the chat service as they’re decoded, and the finished turn is appended to the conversation store. I’d be explicit about which number I’m optimizing, because it isn’t how long generation takes end to end — it’s time to first token. So every turn logs TTFT, tokens per second, cost and quality signals to telemetry, and that feedback is what I’d tune the router and the cache thresholds against.

    Built in step 7: Stream tokens, watch TTFT
  9. 39–45 min

    Close on the trade-off

    To close: every layer here is trading something. Statelessness trades a store lookup for a fleet I can scale and deploy. The router trades a sliver of quality headroom for most of the bill. Batching trades an individual request’s speed for the fleet’s throughput, and the cache trades memory for prefill. The day traffic outruns capacity I’d rather degrade in a deliberate order than let everyone time out equally — bound the queue, let the router quietly downgrade easy prompts, lean on the cache, and shed free-tier traffic before paid. With more time I’d go to multi-modal prompts, where every budget on this diagram gets tighter, and to the eval loop that decides when a routing threshold has drifted.

What this teaches

Learn AI system design by designing ChatGPT itself — a chat product serving hundreds of millions of weekly users and billions of prompts a day. An interactive guide covering stateless chat cells, sharded conversation storage, per-turn context rebuilding under a token budget, model routing between small and frontier models, prompt/prefix caching, continuous batching on a GPU fleet, token streaming, and the load-shedding playbook for a capacity crunch.

Key takeaways

  • Stateless cells — any server serves any user; state lives in stores
  • Conversation store — sharded by conversation id, hot/cold tiered
  • Context builder — model is stateless — rebuild the prompt every turn
  • Model router — cheapest capable model; the #1 cost lever
  • Prompt cache — reuse KV state of unchanged prefixes
  • Continuous batching — dozens of users per GPU, join mid-flight
  • KV-cache memory — the real per-GPU capacity limit
  • Stream + TTFT — perceived speed = time to first token

Concepts covered

  • "Design ChatGPT" — what actually makes it hard?
  • Edge in front, stateless cells behind
  • The conversation store
  • Rebuild the context every turn
  • The model router
  • Prompt / prefix caching
  • Continuous batching, or bankruptcy
  • Stream tokens, watch TTFT
RUN IT YOURSELF

The turn your system prompt falls out of the window

The context builder from step 3, running for real in your browser: one 26-turn conversation, rebuilt every turn under a token budget by two truncation policies. It prints the turn naive truncation evicts the system prompt, the reply that changes because of it, and what each policy bills the GPU for prefill. Change BUDGET at the top, hit Run, and watch the eviction turn move.

HOW TO READ THE CODE — 5 IDEAS
  1. Naive truncation is messages[-k:] over a flat list — and messages[0] is the system prompt, so the pinned instruction is the first thing evicted (step 3).
  2. Nothing raises. The errors column is counted, not asserted: the assistant simply starts answering a dosing question it was told to refuse.
  3. A prefix cache reuses a common prefix, not a common set — so a window whose start moves invalidates the whole thing (step 5).
  4. Naive slides its start on nearly every turn; pinning the head and folding the middle in BLOCKs moves it once per block, which is the entire gap in prefill billed (steps 5–6).
  5. Pinning has a floor — system prompt + summary + KEEP_RECENT. Drive BUDGET under it and the context prints >budget, because a pin is a promise the budget cannot always keep.
CPython · WebAssembly
built to be reasoned about, not memorized — route a prompt, spike the traffic, run the quiz.
Finished this one? 0 / 65 AI System Designs done
NextDesign a RAG Pipeline Next in “AI for Product Managers” · step 6 of 14

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs