← System design

Infrastructure

Design ChatGPT

A chat assistant looks like a text box and a stream of words. Behind it is a fleet of thousands of GPUs where the scarce resource is not compute but memory for each conversation's attention cache. The design problem is to keep those GPUs busy with many conversations at once, start every reply quickly, stream tokens smoothly, and stop unsafe output before the user sees it, all while the bill is measured in GPU hours.
Core: paged KV cache + continuous batching + cache-aware routing40M daily usersAbout 4M output tokens a second at peakFirst token in under a second

01

Requirements

02

Requirements

Functional
  • Users hold multi-turn conversations; each reply streams token by token as it is generated.
  • Conversation history is stored, listed, searched, renamed and deleted by the user.
  • Users can stop a reply mid-stream, regenerate it, or edit an earlier message and branch from there.
  • Several models are offered: a fast default and a larger, slower one, with file and image inputs on some.
  • Free and paid tiers get different limits, priorities and model access.
  • Inputs and outputs pass safety checks; developers get the same models through a metered API.
Non-Functional
  • Time to first token under 1 second at p95 for the default model with a normal prompt.
  • Steady streaming after that: at least 30 tokens per second per reply, with no long pauses.
  • GPU utilisation high enough that the fleet is affordable; idle accelerators are the biggest waste in the system.
  • A failed GPU or node costs users at most a retried reply, never a lost conversation.
  • Usage is metered to the token for billing and quotas, with no unbilled replies.
  • Paid users keep their latency targets during traffic spikes; free users may queue.
Scope it early

Ask whether you are designing the product (conversations, storage, streaming) or the inference platform underneath. Most interviewers want both, weighted toward serving: how requests reach GPUs and how the GPUs stay efficient. Training is almost always out of scope.

02

Sizing the fleet

03

Sizing the fleet

Assume 40M daily users sending 8 messages each, a default model in the 70-billion-parameter class served in 8-bit weights on 8-GPU nodes, an average prompt of 2,500 tokens once history is included, and an average reply of 450 tokens.

QuantityWorkingResult
Replies per day40M x 8320M
Average and peak reply rate320M / 86,400 s, peak 2.5xabout 3,700 / s, peak about 9,300 / s
Output tokens at peak9,300 / s x 450about 4.2M tokens / s
Prompt tokens at peak9,300 / s x 2,500, half served from prefix cacheabout 11.6M computed tokens / s
KV cache per token2 x 80 layers x 8 KV heads x 128 dims x 2 bytesabout 320 KB
KV cache for one 4K conversation4,096 x 320 KBabout 1.3 GB
Memory on one node8 x 80 GB minus about 70 GB of weights and overheadabout 500 GB for KV cache
Concurrent sequences per node500 GB / 1.3 GBabout 380 at 4K context
Decode throughput per nodelarge batches, memory-bandwidth boundabout 4,000 tokens / s
Decode nodes at peak4.2M / 4,000, plus 20% headroomabout 1,260 nodes, 10K GPUs
Conversation storage320M messages / day x about 3 KBabout 1 TB a day
What the table tells you

Memory, not arithmetic, sets the number of users each node can serve. A node has the compute for thousands of concurrent replies, but its KV cache fits only a few hundred long conversations. Nearly every optimisation below exists to fit more conversations into that memory or to avoid recomputing it.

Text storage is a rounding error

A terabyte of conversations a day is ordinary database work. The cost of the system is almost entirely GPUs, so the design is judged by tokens served per GPU hour.

03

API design

04

API design

POST/v1/conversations/{conversation_id}/messagesSend a message and stream the reply
// Accept: text/event-stream { "parent_id": "msg_301", // lets edits branch the conversation "content": [{ "type": "text", "text": "Summarise this contract in five bullets" }, { "type": "file", "file_id": "file_88a" }], "model": "default", "request_id": "req_5b2f" // retries resume the same reply } // server-sent events event: start data: {"message_id":"msg_302","model":"default-2026-09"} event: delta data: {"i":0,"text":"Here"} event: delta data: {"i":1,"text":" are the"} event: done data: {"finish":"stop","usage":{"input":3120,"cached":2048,"output":212}}
POST/v1/messages/{message_id}/cancelStop generating
// frees the sequence's GPU slot at the next decode step; the partial text is kept { "status": "cancelled", "output_tokens": 97 }
GET/v1/messages/{message_id}/stream?from=140Resume a dropped stream
// tokens already generated are buffered for a few minutes, so a phone that changed networks // picks up at token 140 instead of paying for a new reply
GET/v1/conversations?cursor=c_19&limit=30List conversations
{ "items": [ { "id": "cv_77", "title": "Lease review", "updated_at": "2026-10-01T08:14:00Z" } ], "next": "c_49" }

Streaming uses server-sent events over HTTP rather than WebSockets. The traffic is one request followed by a one-way stream, SSE works through ordinary proxies and load balancers, and each event carries an index so a client can resume after a dropped connection without duplicating text.

04

Architecture

05

Architecture

Request path from the chat box to a GPU and backDiagram
flowchart LR
	U["Web and mobile apps"] --> GW["API gateway: auth, quotas"]
	GW --> CS["Conversation service"]
	CS --> DB[("Conversation store")]
	CS --> IM["Input safety check"]
	IM --> RT["Inference router and queues"]
	RT --> PF["Prefill workers"]
	PF --> DC["Decode workers with KV cache"]
	DC --> OM["Streaming output moderation"]
	OM --> GW
	DC -. "token counts" .-> MQ[["Usage events"]]
	MQ --> BL["Metering and billing"]
	RT --> MR[("Model registry and capacity")]

The conversation service owns the product side. It loads the conversation branch, adds the system prompt and any memory or tool definitions, trims or summarises older turns to fit the model's context window, and saves the user's message before generation starts. It then calls the inference router with a fully built prompt and streams whatever comes back to the client.

The router decides where a request runs. It picks a model version and region, places the request in a priority queue for its tier, and chooses a replica, preferring one that already holds this conversation's earlier tokens in its prefix cache. It also enforces admission control: if every replica's KV memory is full, new requests wait in the queue rather than evicting someone mid-reply.

Inference itself splits into two kinds of work. Prefill processes the whole prompt in one parallel pass and is limited by compute. Decode generates one token at a time per sequence and is limited by how fast weights and cache can be read from GPU memory. Large deployments often run them on separate pools and hand the KV cache from one to the other. Tokens leave decode workers through a streaming moderation layer, and usage counts flow to metering on a queue.

Conversation service

Stateless, horizontally scaled. It decides what the model sees, which makes it the place for context truncation, memory and tool wiring.

Router

Queues by tier, routes by prefix affinity and load, and sheds free traffic first. Most latency spikes are decided here, not inside the GPU.

Inference replicas

One model copy spread over the 8 GPUs of a node with tensor parallelism. Each replica runs a scheduler that batches every active sequence on every step.

Safety layers

A fast classifier on input, policy in the system prompt, and a streaming classifier on output that can cut a reply off within a few tokens.

05

Deep dive: what keeps the GPUs busy

06

Deep dive: what keeps the GPUs busy

The economics in one sentence

Decoding one token for one user reads the entire model's weights from memory, so the only way to make decoding cheap is to decode for many users in the same step. Everything here is about growing that batch without breaking latency.

Lifecycle of one reply inside a replicaDiagram
flowchart TD
	A["Request admitted by router"] --> B{"Prefix already cached?"}
	B -->|yes| C["Reuse cached KV blocks"]
	B -->|no| D["Prefill prompt, write KV blocks"]
	C --> E["Join the running decode batch"]
	D --> E
	E --> F["Each step: one token for every sequence"]
	F --> G{"Stop token, limit or cancel?"}
	G -->|no| F
	G -->|yes| H["Free KV blocks, record usage"]

The KV cache

A transformer computes, for every token, key and value vectors at every layer. Generating token 501 needs the keys and values of tokens 1 to 500. Recomputing them for each new token would make generation quadratic, so servers keep them in GPU memory as the KV cache. With the model assumed above, that is roughly 320 KB per token, or 1.3 GB for a 4K-token conversation.

Allocating that memory as one contiguous block per request wastes a lot, because you do not know in advance how long the reply will be. Paged attention, popularised by vLLM, borrows from operating systems: split the cache into fixed blocks of, say, 16 tokens, keep a block table per sequence, and allocate blocks as the sequence grows. Fragmentation drops to almost nothing, and blocks can be shared between sequences that start the same way.

Continuous batching

The naive approach groups requests into a batch, runs it until the longest reply finishes, then starts a new batch. Short replies finish early and leave their slots idle, and new requests wait for the slowest one. Continuous batching schedules at the level of single decoding steps instead. On every step, finished sequences leave the batch and queued ones join, so the batch stays full and nobody waits for a stranger's essay to finish.

The scheduler also has to fit prefill in. A big prompt prefill can stall decoding for every other sequence on the replica for hundreds of milliseconds, which users see as a stutter. Two common fixes: chunked prefill, which splits long prompts into pieces interleaved with decode steps, and running prefill on separate workers and shipping the finished KV blocks to a decode worker over fast interconnect.

When memory runs out mid-flight, the scheduler preempts the newest or lowest-priority sequences, either moving their blocks to CPU memory or dropping them to recompute later. Doing that on purpose, under policy, beats crashing with an out-of-memory error.

Prefix caching and cache-aware routing

Every turn of a conversation resends all previous turns, and every request shares the same long system prompt. Recomputing those tokens is wasted work. With prefix caching, KV blocks are indexed by a hash of the tokens they cover, so a new request that starts with a known prefix reuses those blocks and only prefills the new part. For a chat product, half or more of prompt tokens can come from cache, which cuts time to first token and prefill compute in step.

This only pays off if the request lands on the replica that has the blocks. The router therefore uses consistent hashing on conversation id, with a load cap, so follow-up turns usually return to the same replica. If that replica is overloaded, the request goes elsewhere and pays the full prefill; correctness never depends on the cache.

Splitting the model across GPUs

A model too large for one GPU is split. Tensor parallelism cuts each layer's matrices across the GPUs of a node; every layer needs an all-reduce between them, which is fine over the very fast links inside a node and slow across nodes. Pipeline parallelism places groups of layers on different nodes; communication is light, but each request passes through the stages in sequence. A common setup is tensor parallelism inside a node and pipeline parallelism only when the model will not fit in one node. Mixture-of-experts models add expert parallelism, routing each token to the GPUs holding its chosen experts.

Two latency numbers, not one

Users feel two things. Time to first token depends on queueing and prefill: how long until text starts appearing. Time per output token depends on decode batch size: how smoothly the text flows. Bigger batches improve throughput and hurt per-token speed, so the scheduler caps batch size per replica to keep paid users above their tokens-per-second target. Speculative decoding helps the second number: a small draft model proposes several tokens, the large model verifies them in one pass, and accepted tokens come almost for free.

Safety runs alongside, not after

Waiting for a full reply before checking it would destroy streaming. Instead, an output classifier scores the stream in windows of a few dozen tokens and can stop generation, retract the partial text and show a refusal. The input check runs before the request reaches the GPU, so blocked prompts never cost inference time.

06

Anti-patterns

08

Anti-patterns

!
Fixed batches that run until the slowest reply ends

GPU slots freed by short replies sit idle, and queued users wait for an unrelated long reply. Utilisation and latency both suffer.

Instead: schedule per decode step so sequences join and leave the batch continuously.
!
Recomputing the prompt for each new token

Each step repeats work on every earlier token, so cost grows with the square of the length and long replies become unaffordable.

Instead: keep the KV cache in paged blocks, and reuse shared prefixes across requests.
!
Checking safety only on the finished reply

Either streaming is disabled, or harmful text has already appeared on screen by the time the check runs.

Instead: screen input before inference and classify the output stream in small windows, with the ability to stop and retract.
!
Round-robin routing to replicas

Follow-up turns land on replicas that lack the conversation's cached prefix, so every turn pays full prefill and first tokens slow down.

Instead: route by conversation with a load cap, falling back to the least loaded replica when needed.

07

Tradeoffs and design choices

09

Tradeoffs and design choices

Chosen
Large decode batches with a per-tier speed floor

Throughput per GPU climbs with batch size, which is what makes the service affordable. Capping batch size per replica keeps paid users above their tokens-per-second target.

Balances cost and feel
Alternative
Small batches for maximum speed

Each user sees very fast streaming, but the fleet needs several times more GPUs for the same traffic.

Premium or low-latency tier only
Chosen
Separate prefill and decode pools

Long prompts no longer stall other users' streams, and each pool runs on hardware sized for its bottleneck. The cost is moving KV blocks between machines, which needs fast networking.

At large scale
Alternative
One pool with chunked prefill

Simpler to operate and no cache transfer. Prefill still competes with decode on the same GPUs, which shows up as jitter under load.

Good starting point
Chosen
Route the default to a smaller model, escalate on demand

Most questions are answered well by the fast model at a fraction of the cost. Users or a router can opt into the large model for hard tasks.

Cuts cost sharply
Alternative
Always use the largest model

Best quality on every reply, with slower streaming and several times the GPU cost per token.

Wasteful for simple chats
Chosen
SSE streaming with resumable indexes

Plain HTTP, friendly to proxies, and cheap to reconnect. Two-way signals such as cancel go through a separate request.

Fits one-way output
Alternative
WebSockets

True two-way channel that suits voice and live collaboration. More connection state to manage at the gateway for little benefit in text chat.

Needed for voice

08

Failure modes

10

Failure modes

🔥
A GPU fails during generation

One bad GPU takes down its whole tensor-parallel replica, cutting off a few hundred streams at once.

The router marks the replica dead and replays each affected request elsewhere from the prompt plus the tokens already sent. Clients see a short pause, not an error, because the stream index lets the server continue where it stopped.
🧠
KV memory runs out under long contexts

A burst of 100K-token document chats fills the cache, and new requests start failing or old ones crash.

Admission control reserves memory before accepting work, long-context requests go to a dedicated pool, and the scheduler preempts low-priority sequences deliberately instead of failing at random.
🌊
Traffic surges back after an outage

Millions of users retry at once when the service returns, and queues fill faster than replicas can warm their caches.

Clients back off with jitter, the gateway sheds free-tier traffic first, and the router admits paid traffic up to capacity while showing others a wait message.
🎭
A prompt injection slips past the filters

Instructions hidden in an uploaded file convince the model to ignore its rules or leak tool outputs.

Treat file and tool content as untrusted data in the prompt, restrict which tools can act on it, run output classification regardless of input, and log for review.
🧾
Usage metering falls behind

The usage queue backs up for an hour. Quotas stop enforcing and billing misses tokens.

Count tokens on the replica and write them with the final event, keep the queue durable with long retention, and enforce quotas from a fast local estimate that the meter reconciles later.

09

Interview tips

11

Interview tips

1

Start with the KV cache. Explain what it stores and size it with numbers. Showing that memory limits concurrency frames everything after it.

2

Name continuous batching. Contrast it with fixed batches and describe how prefill is scheduled around decode.

3

Separate the two latencies. Time to first token and time per token have different causes and different fixes. Interviewers listen for both.

4

Explain the parallelism choice. Tensor parallel inside a node, pipeline across nodes only when needed, and why.

5

Make routing smart. Prefix-aware routing and tiered queues are where product and infrastructure meet.

6

Layer safety. Input screening, system policy and streaming output checks. Never a single filter at the end.

10

Evolution

13

Evolution

Stage 1: a prototype
One GPU, one request at a time

A single server runs the model per request. Most of the GPU sits idle, and the second user waits for the first.

Stage 2: early users
KV cache and simple batching

Keeping attention state removes recomputation, and grouping requests raises throughput, at the cost of uneven latency.

Stage 3: real traffic
Continuous batching and paged memory

Step-level scheduling and block-based KV cache multiply the number of conversations per node. Streaming and output moderation arrive.

Stage 4: a fleet
Router, tiers and prefix caching

A routing layer with priority queues and conversation affinity spreads load across regions, with tensor parallelism inside each node.

Stage 5: cost at scale
Model routing, disaggregation and speculation

Small models handle easy requests, prefill and decode run on separate pools, and speculative decoding speeds up the large models.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.