System design interview

Design an LLM chat service

Streamed replies, continuous batching, KV-cache memory, prefix caching and quotas.

Overview
Chat appQuotaserviceAPI gatewayConversationstoreSchedulerToken bufferSmall-modelGPUsLarge-modelGPUsChat appQuota servicetoken budgetsAPI gatewayConversationstoreSchedulerToken bufferSmall-model GPUsfallback poolLarge-model GPUs

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. The user sends a message in a conversation. The reply will come back on the same request as server-sent events.
  2. The gateway loads the saved history, saves the new user message, and builds the prompt from the system prompt, recent turns and the message.
  3. It reserves tokens: 1,800 for the prompt plus a cap of 2,000 for the reply, 3,800 in all, against the user's budget.
  4. It sends the prompt to the scheduler, tagged with the user's plan.
  5. The scheduler picks the replica that holds the cached start of this prompt and has room. The replica processes the prompt, then joins its running batch.
  6. Every 36 ms or so the replica produces one more token for the request, and sends it back.
  7. The scheduler passes the tokens to the gateway.
  8. The gateway streams them to the app as events with increasing IDs, and appends each batch to the token buffer.
  • Chat app
  • Quota service: token budgets
  • API gateway: stateless, streams
  • Conversation store: one row per turn
  • Scheduler: queue, prefix routing
  • Token buffer: replay for resume
  • Small-model GPUs: fallback pool
  • Large-model GPUs: continuous batching
On this page

In 2 minutes

I would put a stateless gateway in front of a scheduler and GPU replicas. The gateway streams server-sent events, loads history from a conversation store, and reserves tokens from a quota service before generating: the prompt plus a cap on the reply, settled to the real count afterwards. Each token batch is also appended to a token buffer with a sequence number, so a client that reconnects with its last event ID replays what it missed, and a dead gateway's reply restarts from the saved tokens.

A stop cancels the sequence within one step and frees its cache. On the GPUs, I would use continuous batching. With a 70-billion-parameter model, the KV cache takes about 320 KB a token, so 150 GB holds about 458,000 tokens and 183 requests, and a replica makes about 5,000 tokens a second instead of 57.

Paged memory raises concurrency by 60%, and a shared prefix cache, with follow-up turns routed to the same replica, cuts the need from about 19 replicas to 12. When the GPUs are full, a priority queue serves paid users first. Free requests that would wait too long go to a smaller model, or are refused with a retry time.

Hide the answers and test yourself

Step 1 of 6: Clarify the scope

Interviewer asks

Design a chat assistant backed by a large language model that runs on GPUs. Users send a message and see the reply appear word by word. Conversations are saved, and each user has a fair usage limit. Keep the design independent of any one model vendor.

Questions to ask first

Ask these before you draw anything. Under each one is why its answer changes the design.

Does the reply stream token by token, and may the user stop it halfway?

Streaming sets the connection type and how partial replies are saved. Stop must free the GPU, or abandoned replies waste the scarcest resource.

How long are conversations, and do users paste long documents?

Prompt length drives prefill cost and KV-cache memory, which together decide how many requests one GPU group can serve.

How many users are there, and what is the peak request rate?

GPU count follows peak demand, and GPUs are too costly to leave idle, so scaling and queueing matter.

What limits apply: per user, per plan, per minute, per day?

Cost scales with tokens, not with requests, so limits should count tokens.

When every GPU is busy, should a request wait, be refused, or go to a smaller model?

Overload behaviour is a product choice. It shapes the queue and the fallback path.

Requirements

Functional

  • Accept a message in a conversation and stream the reply as it is generated.
  • Save each turn, and rebuild the prompt from the saved history.
  • Let a user stop a reply, and resume one after a dropped connection.
  • Enforce token quotas per user and plan, and queue or degrade when the GPUs are full.

Non-functional

  • First token within 1 second at p95 when load is normal.
  • A steady stream of about 25 tokens a second for each reply.
  • The API tier is stateless: any gateway can serve any request, and a gateway crash loses no saved turn.
  • Stop frees the GPU work within 1 second.
  • Capacity for 35 requests a second at the peak, with the GPUs at no more than 70% load.

Step 2 of 6: Size it

The GPUs are the cost, and two things set how much they can do: how many requests fit in KV-cache memory, and how much prefill work each request brings. Batching, paged memory and prefix caching move those two numbers, so they matter more than anything in the API tier.

Assumed for the exercise, not facts about a real system:

  • A 70-billion-parameter model with 16-bit weights, so 140 GB. A replica is 4 GPUs of 80 GB with 2 TB/s memory bandwidth each. The model has 80 layers and 8 key-value heads of size 128. These are assumed numbers, not a specific product.
  • 30 GB of each replica is kept for working memory, and the rest of the 320 GB, after the weights, is for the KV cache.
  • 1 million messages a day, with a peak 3 times the average. A prompt averages 2,000 tokens, of which 1,000 are a shared system prompt. A reply averages 500 tokens.
  • Prefill, which is compute-bound, sustains about 1,000 TFLOP/s on a replica, and a token costs about 140 GFLOP. Run the GPUs at 70% load.
KV cache per token
2 (keys and values) × 80 layers × 8 heads × 128 × 2 bytes
327,680 bytes ≈ 320 KB
Cache room per replica
320 − 140 − 30 = 150 GB ÷ 320 KB ≈ 458,000 tokens ÷ 2,500 tokens per request
≈ 183 requests running at once
Decode speed per replica
Each step reads 140 GB of weights + 150 GB of cache = 290 GB ÷ 8 TB/s ≈ 36 ms, so 28 tokens/s for each of 183 requests
≈ 5,000 tokens/s, against ≈ 57/s one request at a time
Peak demand
1M ÷ 86,400 s ≈ 11.6/s; × 3 ≈ 35/s; × 500 tokens
≈ 35 requests/s, 17,500 output tokens/s
Prefill load at the peak
1,000 new tokens × 140 GFLOP = 140 TFLOP ÷ 1,000 TFLOP/s = 0.14 s; × 35/s
≈ 4.9 replicas of compute
Replicas needed
Decode 17,500 ÷ 5,000 = 3.5; + prefill 4.9 = 8.4; ÷ 0.7 load
≈ 12 replicas (48 GPUs). Without a prefix cache, prefill is 9.8, so ≈ 19

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

The Chat app sends a message to the API gateway, a stateless tier. The gateway loads the conversation's history from the Conversation store, builds the prompt, and asks the Quota service to reserve tokens. It then hands the prompt to the Scheduler.

The Scheduler owns a priority queue and knows each GPU replica's load. It sends the request to a replica in the Large-model GPUs, picking one that already holds the cached start of the prompt. The replica adds the request to its running batch and streams tokens back through the scheduler to the gateway, which streams them to the app. When the large pool is full and the wait is long, the scheduler can use the Small-model GPUs.

The gateway also appends each batch of tokens to the Token buffer, so a client that reconnects can replay what it missed. When the reply ends, the gateway saves the turn and tells the quota service how many tokens were really used.

Other paths through the system

Resume, finish and settle

  1. The phone loses its connection at token 240. When it reconnects, it sends the last event ID it saw, and may reach another gateway.
  2. The gateway asks the token buffer for the tokens after 240 for this reply.
  3. The buffer returns 22 tokens the app missed, and the gateway goes on streaming live tokens.
  4. When the reply ends at 640 tokens, the gateway saves it as the assistant turn, marked complete.
  5. It settles the quota: 1,800 plus 640 tokens used, so the 1,360 unused tokens of the reservation go back to the budget.

Overload and fallback

  1. At a traffic spike, a free-plan request reaches the scheduler while the large pool is full.
  2. Estimated queue time is 40 seconds, over the 10-second limit for free plans, so the scheduler sends the request to the small-model pool.
  3. The small model streams its tokens about four times faster.
  4. The gateway marks the reply as from the faster model, so the app can say so.

Step 4 of 6: Go deep on the hard part

Streaming, stopping and resuming

Interviewer asks

How do you stream a reply through a stateless tier, stop it cleanly, and resume after a dropped connection?

The channel. Use server-sent events: a plain HTTP response that stays open and carries a stream of events.

Read more: Streaming, stopping and resuming

It passes through proxies and load balancers, and a browser reconnects by itself, sending the last event ID it saw. The user's message is a separate request, and so is a stop. Use a WebSocket only if the app needs two-way traffic during a reply, such as voice, since it needs sticky connections and more machinery.

Stateless gateways. A gateway keeps nothing that another one could not rebuild. While it streams, it appends each batch of tokens to the token buffer, keyed by conversation and reply, with an increasing sequence number and a TTL of 10 minutes. The event ID sent to the app is that number.

  • Client drops. The client reconnects, to any gateway, with its last event ID. The gateway reads everything after it from the buffer and then follows live. The generation never paused, so nothing is lost.
  • Gateway dies. The stream to the replica breaks. The next gateway takes the tokens saved in the buffer, and starts a new generation whose prompt is the original prompt plus those tokens. The replica often still has the original prompt in its cache, so the restart is cheap, and the saved tokens stay as they were.

Stop. A stop request reaches the gateway, which sends a cancel to the scheduler and on to the replica. The replica drops the sequence from its batch before the next step and frees its cache blocks, within one step of about 36 ms. The tokens already made are saved as the reply, marked stopped, and the quota is settled for what was produced. If a client has been gone for 30 seconds with no reconnect, cancel in the same way. Otherwise an abandoned reply holds GPU memory for up to a minute.

Saving turns. Save the user message before generation begins, so it is never lost. Save the assistant message when it completes, is stopped or fails, with a status. The key is conversation and turn number, so a retry overwrites the same row. When building the prompt, count tokens with the model's own tokenizer and keep as many recent turns as fit a budget, such as 6,000 tokens. Summarise older turns, or drop them.

Serving on GPUs: batching and KV-cache memory

Interviewer asks

What limits how many requests a GPU group can serve, and how do you raise it?

A model makes a reply one token at a time.

Read more: Serving on GPUs: batching and KV-cache memory

Each step reads all the weights, 140 GB, from memory. That read, not arithmetic, takes the time. One request alone gets a token every 18 ms, about 57 a second, and the GPUs sit nearly idle. The fix is to serve many requests per step.

  • Continuous batching. Static batching waits until every request in the batch finishes, so short replies wait for long ones. Instead, decide at every step: finished requests leave, and waiting ones join. The weights are read once for the whole batch. At 183 requests in the batch, a step takes about 36 ms, and every request gets 28 tokens a second. The replica makes about 5,000 tokens a second, 88 times as many as one at a time.
  • KV cache. To avoid recomputing the past, the model keeps the keys and values of every earlier token. That is 320 KB a token in our assumptions. With 150 GB free after the weights and working memory, the cache holds about 458,000 tokens. That, not compute, is the real limit on concurrency.
  • Paged memory. Reserving 4,000 tokens for every request fits 114 requests. Allocate the cache in small blocks as a request grows, and 458,000 ÷ 2,500 fits 183, which is 60% more.
  • Prefix caching. Every request starts with the same 1,000-token system prompt. Compute its cache once, and let all requests share those blocks. The shared copy is about 330 MB. A private copy for each of 183 requests would be 60 GB, 40% of the cache. The estimate leaves this extra room unused.

Routing. A prefix cache helps only if the request lands on a replica that holds it. Send the follow-up turns of a conversation to the replica that served the last one, by consistent hashing on the conversation ID. Its earlier tokens are likely still cached. If that replica's queue is much longer than the others, send the request elsewhere. A hot replica is worse than a cold cache.

Prefill and decode. Processing the prompt, called prefill, is compute-bound and takes 0.14 s for 1,000 tokens. Run it in the same step as other requests' decode, and all of them stall for 140 ms and the stream stutters. Split prefill into chunks of 256 tokens and mix them in with decode steps. Or run prefill on separate replicas and move the cache over the network: 1,000 tokens is about 330 MB, 7 ms at 50 GB/s. Prefill is also why the replica count includes 4.9 replicas of compute besides decode.

Limits, queueing and cost

Interviewer asks

How do you keep usage fair and costs bounded, and what happens when the GPUs are full?

Count tokens. A short question that gets a long answer costs far more than a long question that gets a short answer.

Read more: Limits, queueing and cost

So quotas count tokens, input and output, per user. Give each plan a daily budget and a per-minute limit, for example 30,000 tokens a day on a free plan and 500,000 on a paid one. Use a token bucket for the per-minute limit, which allows a burst. Also cap each user at 2 replies running at once.

Reserve, then settle. The reply's length is unknown at the start. Before generating, reserve the prompt size plus a cap on the reply, say 1,800 + 2,000 = 3,800 tokens, with one atomic update in a fast store. If the budget is too low, refuse at once with a clear message. After the reply, settle: charge the real use, 1,800 + 640 = 2,440, and give back the 1,360 unused. Give a reservation a 5-minute expiry. If a gateway dies, the budget is not locked for good, and a reaper settles from the replica's usage log.

Queue and priority. At full load the 12 replicas serve about 50 requests a second, against the 35 peak. When a spike passes that, requests wait. Keep a queue per plan, serve paid first, and within a lane go first in, first out. Estimate each request's wait as queue length divided by the service rate, and show it. A spike of 70 requests a second for one minute adds (70 − 50) × 60 = 1,200 waiting requests, and the last one waits about 24 seconds. Set a limit per plan, say 10 seconds for free, and past it choose: send to the small model, or refuse with a retry-after time. A refusal is better than a request that waits 3 minutes and times out after using the GPU.

Graceful degradation. A smaller model on its own pool answers about four times faster and costs a fraction as much. Use it for overflow, and for short or simple requests chosen by a cheap classifier. Tell the user which model answered. Add a lower tier of last resort: a cached or canned reply for the most common prompts.

Cost. At an assumed 2.50 dollars a GPU-hour, 48 GPUs cost 120 dollars an hour, or 2,880 a day. For 1 million messages, that is about 0.29 cents a message. The load is far lighter at night, so scale replicas with traffic, keeping warm capacity because loading 140 GB of weights into a new replica takes minutes.

Step 5 of 6: Name the trade-offs

Reply channel

Server-sent events plus separate requests for messages and stop over A WebSocket for everything

It works through ordinary proxies, and it reconnects by itself. The cost is that two-way features need extra requests, or a socket later.

Batching

Continuous batching, with paged KV-cache memory over One request at a time, or fixed batches

A replica serves about 183 requests at once and makes 5,000 tokens a second, not 57. The cost is a more complex server and added delay from sharing steps.

Prompt start

A shared prefix cache, with routing by conversation over Recompute every prompt from scratch

It cuts the estimate from about 19 replicas to 12. The cost is routing that must not overload one replica, and cache blocks to manage.

Quota

Reserve tokens first, then settle over Charge only after the reply

A user cannot overspend with many parallel requests. The cost is a refund step, and expiry rules for reservations that never settle.

Overload

Priority queue, then a smaller model or a refusal over An unbounded queue

Waits stay bounded and paid users go first. The cost is a lower-quality answer for some free users at peak.

Step 6 of 6: Say what breaks next

Questions an interviewer asks once the design is on the board. Answer each one out loud first.

How do you roll out a new model version?

Load it on a few new replicas and send a small share of traffic to them, for example 1%, tagging every reply with the model version. Compare quality ratings, refusals and latency before widening. The scheduler keeps the old pool warm so rollback is a routing change.

How do you handle very long prompts, such as a pasted document?

Long prompts cost prefill time and a large share of the cache. Charge them by tokens, cap the size, and split the prefill into chunks so they do not stall other replies. For large documents, retrieve the relevant parts rather than sending everything.

What happens when a GPU or a replica fails?

The scheduler sees the health check fail and stops sending it requests. The streams on it break, and the gateways restart each one from the saved tokens on another replica. The user sees a short pause, not an error.

How do you keep one user's data from showing up in another's reply?

Prefix caching must only share the system prompt, never user text across accounts. Key shared blocks by a hash of their exact tokens, and never reuse a block for a different tenant's content. Clear a sequence's blocks when it ends.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.