InterviewKata
System design · Worked design D23 of 46 · Data and infrastructure · Long form

Design an LLM chat platform

A chat assistant looks like a simple request and response, but each reply is a long, expensive computation on scarce GPUs, streamed a few words at a time for ten or twenty seconds. The design problems are unusual: GPU memory, not CPU, limits how many people you can serve; idle GPUs burn money; and keeping the first word fast matters more than total speed. This page has material for a full 60–90 minute interview.

1. The prompt

"Design a chat assistant like ChatGPT: people type a message, the model's reply streams back word by word, and their conversations are saved."

The surrounding product (accounts, conversation lists) is ordinary. The heart is the inference tier: how requests share GPUs, how much memory each conversation holds while it generates, how to keep the first token fast under load, and how to fail gracefully when GPUs run out. Say early that GPU cost dominates everything; it frames every trade-off that follows.

2. Frame the problem

Ask these in the first five minutes. Guess each answer and its effect before opening it.

Do we run our own models, or call a model provider's API?

We serve our own open-weight models (say a 70B-parameter model and a small fast one) on our GPUs, and may route some traffic to an external provider.

Running our own models puts GPU serving, batching and memory in scope. With only external APIs, the design would be about routing, quotas and cost.

How many users, and how bursty?

About 20M daily users sending ~10 messages each, with a strong daily peak about 3× the average.

We get generations per second and output tokens per second at peak, and from those the GPU count, the biggest cost line.

Must replies stream?

Yes. The first words should appear within about a second.

Two latency numbers matter: time to first token (TTFT) and tokens per second while streaming. The API must be a stream.

How long can conversations get?

The model supports 128K tokens of context, but most conversations stay under 8K.

Context length decides KV-cache memory per conversation, which limits how many can share a GPU.

Are there free and paid tiers?

Yes. Paid users get priority, larger limits and the big model; free users get the small model when busy.

We need priority queues, per-tier token limits and a fallback path.

Tools, web browsing, file uploads?

Not in this version.

Tools add an agent loop and retrieval (Module 29). Keep them out and say they'd sit in front of the same inference tier.

Safety and privacy requirements?

Inputs and outputs are checked against usage policies. Users can delete conversations. Some enterprise customers require that their data is not retained.

We need moderation that works on a stream, deletion that really deletes, and per-tenant retention settings.

Regions?

Users worldwide; GPUs in a few regions.

GPU capacity can't be everywhere. Routing between regions, and the data-residency rules that limit it, are part of the design.

3. What it must do

  1. A user can send a message and watch the reply stream in, and stop it at any time.
  2. Conversations are saved: the user can list them, reopen one and continue it.
  3. The user can regenerate the last reply or switch between available models.

Not now (with a reason):

4. How well it must do it

TargetWhy this number
First token fast: TTFT under 1 s at p95 for paid users, under 3 s for free users speedWaiting with no feedback feels broken; once text flows, people are patient.
Readable stream: at least 30 output tokens a second per reply speedFaster than people read. Beyond that, extra speed matters less than cost.
Degrade, don't fail: 99.9% of messages get a reply; under overload, free traffic falls back to the small model or queues with a visible message surviving failures · load shapeGPU capacity can't be added in seconds, so planned degradation beats errors.
Cost: keep GPUs above ~60% busy at peak, without breaking the latency targets load shapeGPUs are the bill. Idle GPU-hours are money burned.
Conversations kept and private: saved messages are durable; deleted ones are gone from storage within 30 days; no-retention tenants are never stored data safety · rulesUsers treat chats as personal notes; companies have contracts and regulations.
Safe: policy-violating inputs and outputs are blocked or stopped mid-stream securityA public assistant attracts misuse.

5. Numbers that change the design

QuantityWorking→ so we…
Generations20M users × 10 messages = 200M a day ≈ 2,300/s, peak ≈ 7,000/sA large but ordinary request rate for the web tier
Output tokensAverage ~500 tokens a reply × 7,000/s~3.5M tokens a second at peak → so the GPU tier, not the web tier, is the design
Model weights70B parameters × 1 byte (8-bit weights) ≈ 70 GB; an H100 has 80 GB→ so one model replica spans 2 GPUs (160 GB), leaving ~70 GB after overheads for conversations
KV cache per tokenLlama 3.3 70B: 2 (key and value) × 80 layers × 8 KV heads × 128 dims × 2 bytes ≈ 320 KiBAn 8K-token conversation holds ~2.5 GiB while it generates → so ~25 long conversations fit per replica: memory, not compute, limits concurrency
ReplicasIllustrative: a 2-GPU replica with good batching streams ~2,500 tokens/s in total3.5M ÷ 2,500 ≈ 1,400 replicas ≈ 2,800 GPUs at peak → so batching efficiency and caching are worth more than any other optimisation
Conversation storage200M messages a day × ~2 KB~400 GB a day: a sharded store, nothing exotic

The per-replica throughput is an illustrative planning number. Real figures depend on the model, GPUs, quantization and prompt lengths, so say you'd benchmark it. The shape of the conclusion doesn't change: GPUs dominate, and memory per conversation limits batching.

6. The nouns

conversations  conversation_id, user_id, title, model, updated_at          (by user_id, updated_at desc)
messages       conversation_id, message_id (time-ordered), role, content, tokens_in, tokens_out, model, parent_id
generations    generation_id, conversation_id, status, model_pool, replica, queued_at, first_token_at, finished_at
usage          user_id, day, tokens_in, tokens_out                          (token limits per tier)

parent_id lets a regenerated reply become a sibling of the old one, so the user can flip between versions.

7. The contract

REST for conversations; the reply streams as Server-Sent Events on the response to the POST, because it's one-way and long. Many model APIs stream this way. Internally, the chat service calls the inference tier over gRPC with a streaming response. The user comes from the session token; the tier and limits come from the account, never from the client.

POST /v1/conversations                        → 201 {conversation_id}
POST /v1/conversations/{id}/messages          {content, model, client_message_id}
     ← event: message_start  data: {message_id, generation_id, model}
     ← event: delta          data: {text: "Sure"}           (many)
     ← event: message_end    data: {stop_reason: "end", usage: {in: 1840, out: 412}}
POST /v1/generations/{generation_id}/cancel   → stop generating, free the GPU slot
GET  /v1/generations/{generation_id}/stream   Last-Event-ID: 57    → resume a dropped stream
GET  /v1/conversations?cursor=…               → list, newest first
GET  /v1/conversations/{id}/messages?cursor=… → history

// internal (gRPC)
Generate(stream) {model, prompt_tokens, max_tokens, priority, cache_key} → stream of tokens

Data flow: (1) check limits → (2) load the conversation and build the prompt (system prompt + history that fits the context) → (3) route to a model pool and queue → (4) a replica runs prefill (reads the whole prompt), then decodes one token at a time → (5) tokens stream back through the chat service to the browser → (6) the finished message and usage are saved.

8. First working version

Start simple: a chat service, a database, and a few GPU servers each running a model server that handles one request at a time. Get the three requests working.

8.1 Sending a message

The chat service saves the user's message, loads earlier messages, and builds the prompt. If the history is longer than the context window, it drops the oldest turns (we improve this in 9.2). It then calls a model server.

8.2 Streaming the reply

The model server produces tokens one by one; the chat service forwards each as an SSE delta event, and on the last token saves the full assistant message with its token counts. The browser renders text as it arrives.

8.3 Listing and reopening conversations

The sidebar lists conversations by (user_id, updated_at desc) with a cursor. Opening one pages through its messages. A short title is generated after the first exchange by a cheap model call, in the background.

Parked for later (section 9 fixes each)

  1. One request at a time per GPU server: the GPU is mostly idle and queues explode at peak.
  2. Long histories exceed the context window, and resending them costs prefill time on every message.
  3. Random routing ignores which replica already has this conversation's prefix cached.
  4. When all GPUs are busy, requests pile up with no priority and no limit.
  5. If the browser connection drops, the reply is lost, and the GPU keeps generating for nobody.
  6. Nothing stops harmful inputs or outputs; deleting a chat doesn't delete everything.

9. Make it hold up

9.1 Sharing GPUs: continuous batching cost · first token fast

Generating a token for one conversation barely uses a GPU's compute: the time goes into reading the model's weights from memory. Reading them once and producing the next token for many conversations at the same time costs little more. Batching is therefore the biggest lever in the whole design.

ApproachWhy
✗ BadOne request at a time per replicaThroughput of one conversation's tokens per step; everyone else waits in a queue. Thousands of GPUs would still not be enough.
~ GoodStatic batching: wait for 8 requests, run them together until all finishBetter throughput, but short replies sit idle until the longest one in the batch finishes, and new arrivals wait for the whole batch.
✓ GreatContinuous (iteration-level) batching: at every decode step, finished sequences leave and waiting ones join, up to the memory limitSlots never sit idle, new requests start within one step, and throughput rises severalfold. The Orca paper (OSDI 2022) introduced this scheduling; serving engines such as vLLM use it.

Two phases share the GPU. Prefill reads the whole prompt at once (compute-heavy, it sets TTFT). Decode produces one token per step (memory-heavy, it sets tokens per second). Engines interleave them; large deployments sometimes run prefill and decode on separate GPU pools, so long prompts don't stall everyone's streaming. Try the three modes in the lab in section 13.

9.2 Memory is the limit: KV cache and context cost · first token fast

9.3 Routing, priorities and admission control degrade, don't fail · first token fast

ApproachWhy
✗ BadRound-robin across replicas and an unlimited queueIgnores how busy each replica is and where caches live; at peak the queue grows until every request times out.
~ GoodLeast-loaded routing by queued tokens and free KV memory; per-tier token limits (tokens per minute) at the gatewayBalances work and stops individual abusers, but treats all traffic as equal when capacity runs out.
✓ GreatCache-aware routing (send a conversation back to the replica holding its prefix, unless it's overloaded), priority queues by tier, and admission control: when the expected wait exceeds the target, free traffic falls back to the small model or gets a clear "busy, try again" messagePaid users keep their latency, free users still get answers, and nobody waits in a queue they'll never leave. Cache hits cut prefill for everyone.

Scaling GPUs is slow: a new replica needs a GPU machine, then minutes to load 70 GB of weights. Autoscale on queue depth and forecast (the daily peak is predictable), keep warm headroom, and use the small model as the pressure valve for sudden spikes.

9.4 Streams that survive bad connections degrade, don't fail

9.5 Storing conversations, and deleting them kept and private

9.6 Safety on a stream safe

9.7 The whole design after the deep dives

One message through every part: limits, prompt building with summaries, cache-aware routing, a continuously batched replica, a resumable token buffer and the saved conversation.

10. Interviewer follow-ups

A new model launches and traffic triples overnight.

GPUs can't triple overnight, so the plan is degradation: per-tier limits tighten automatically, free traffic moves to the small model or a waitlist, paid traffic keeps priority, and the launch is staged by region or tier. In parallel, buy or reserve capacity, and use an external provider as overflow if contracts allow.

A GPU region goes down.

Route new generations to other regions where residency rules allow. Admission control absorbs the drop in capacity by falling back to the small model. In-flight generations in the dead region fail; clients see an error with a retry button, and a retry with the same client_message_id starts cleanly elsewhere. Conversations are replicated across regions, so history is intact.

Cut GPU cost by 30% without hurting quality much.

Raise the prefix-cache hit rate (cache-aware routing, stable system prompts), route easy requests to the small model with a classifier, quantize weights after an evaluation, cap max output length per tier, and use speculative decoding (a small model drafts tokens that the big model verifies in one pass). Measure quality on a fixed evaluation set before and after each change.

How do you roll out a new model version safely?

Offline evaluations first (Module 29), then a canary: a few percent of traffic, with dashboards for refusal rate, user feedback, reply length, latency and cost. Ramp up gradually and roll back instantly by routing. Pin enterprise customers to model versions they've approved.

A user pastes a 100K-token document.

It fits the context window, but its KV cache (~30 GiB) takes almost half a replica's spare memory, and prefill takes seconds. Give such requests their own queue (or a long-context pool) so they don't stall short chats, show a progress state, and charge them against a larger token budget.

Users hit "regenerate" a lot.

Each regeneration reuses the same prompt, so prefix caching makes its prefill nearly free if it lands on the same replica. Keep the old reply as a sibling via parent_id. Rate-limit regenerations on the free tier.

Now add tool use (web search, code execution).

The chat service becomes an agent loop: the model asks for a tool, the service runs it in a sandbox, appends the result and calls the model again (Module 29). The inference tier is unchanged, but each user message may now cost several generations, so limits and cost tracking must count all of them. Tool results also bring prompt-injection risk, so sandbox the tools and treat their output as data, not instructions.

Why not WebSockets for streaming?

The reply is one-way and ends when it's done. SSE over the POST response is plain HTTP, works through proxies and CDNs, and resumes with Last-Event-ID. WebSockets make sense for voice or for two-way live features.

The external provider we use for overflow has an outage.

Treat each provider as a separate pool behind the same router, with a circuit breaker (Module 24). When the breaker opens, the router stops sending traffic there and admission control tightens instead. Keep prompts portable between models, and test the fallback regularly.

How do you know latency is good for users, not just servers?

Measure TTFT and tokens per second at the client and the chat service, per model, tier and region, as percentiles. Track queue time separately from prefill time, so you know whether to add capacity or fix prompts. Alert on burn rate against the TTFT objective (Module 25).

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
A GPU replica crashesIts in-flight replies stop with "try again"Health checks; stream errorsRouter removes it; retries reuse client_message_id; others absorb load
Peak demand exceeds GPUsFree tier gets the small model or a wait messageQueue time per tier; TTFT burn ratePriority queues, admission control, fallback model, pre-scaled headroom
Prefix cache hit rate drops (bad routing deploy)Slower first tokensCache-hit and prefill-time dashboardsCanary routing changes; roll back
Browser disconnects mid-replyReply continues after reconnectReconnects with Last-Event-IDToken buffer with TTL; cancel after 60 s with no reader
Safety classifier is downReplies delayed or blockedClassifier error rateFail closed on high-risk categories; fall back to stricter rules
Bad model versionWorse answers, more refusalsCanary metrics, user feedback rateInstant routing rollback; pinned versions for enterprise
Conversation store slowHistory loads slowly; replies still streamRead latencyRecent turns cached with the session; streaming doesn't depend on the store

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 60-minute interview

0–5Frame the problem: own models? streaming? tiers?
5–12Must do, how well (TTFT, tokens/s, cost), GPU and KV numbers
12–17Nouns, contract with SSE events
17–30First working version 8.1–8.3, parked list
30–52Deep dives: batching (9.1), KV memory and prefix cache (9.2), routing and admission (9.3)
52–60Follow-ups and wrap-up

A 90-minute interview

0–8Frame the problem, including safety and regions
8–18Must do, how well, numbers
18–25Nouns, contract, data flow
25–40First working version
40–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: launch spike, cost cut, tools
85–90Trade-offs, metrics, 10× plan
LevelWhat a strong answer shows
Mid-levelA chat service, conversation storage, a streaming API (SSE), model servers behind a load balancer, and awareness that GPUs are expensive and replies are long-running.
SeniorGPU and KV-cache arithmetic driving the design; continuous batching; prefix caching with affinity routing; priority queues and admission control with fallback; resumable streams and real cancellation; context management.
Staff+Cost as a first-class requirement (utilisation, cache hit rate, model routing, quantization trade-offs with evaluations), prefill/decode separation, capacity planning with slow GPU scaling, multi-provider resilience, safe model rollouts, privacy and retention by tenant.

Common mistakes:

13. Try it, check yourself, read more

GPU batching lab. One model replica for 60 seconds. Requests arrive at random; reply lengths vary from a sentence to a page. Each row is one request: grey while it waits in the queue, coloured while it generates (blue for long replies), red if it was still unfinished when we stopped watching.

Model of one replica: one decode step every ~30 ms (slightly slower with bigger batches), up to 16 sequences in memory at once (8 for static batches). Illustrative, not a benchmark.

What usually limits how many conversations one model replica can serve at once?

Each active conversation holds keys and values for every token; the memory left after the weights caps the batch.

What makes continuous batching better than static batching?

No slot waits for the longest reply in a batch, and new requests start almost immediately.

Why route a conversation's next message back to the same replica?

Prefix caching only helps if the cached blocks are on the replica that serves the request.

Demand exceeds GPU capacity at peak. What should happen?

GPUs take minutes to add; planned degradation keeps everyone served.

The user's connection drops halfway through a reply. How does it continue?

Generation is decoupled from the connection; a reconnect picks up where it stopped.
ResourceWhy
Efficient memory management for LLM serving with PagedAttention (vLLM paper)Why KV memory limits batching, and the paging fix.
Orca: a distributed serving system for transformer-based generative models (OSDI 2022)Iteration-level scheduling: the idea behind continuous batching.
Automatic prefix caching (vLLM docs)Reusing the KV cache of shared prompt prefixes.