Design an LLM Chat Service

Design a consumer AI chat product: streamed replies that survive reconnects, scarce GPU capacity, token budgets, context assembly, safety and cost.

Case study33 minLesson 10 of 10
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

Maya types "Plan three days in Lisbon" and the answer starts appearing word by word less than a second later. She closes the app halfway through, opens it on the train, and the reply is there, finished. A consumer AI chat assistant is the kind of product this question means.

It looks like a chat app with a bot on the other end, but the hard parts differ: the "other person" is a pool of GPUs that is expensive, slow to grow and easy to overload, each reply takes seconds, and each request carries the conversation so far. The steps follow A Framework for Any Design. Try each "Your turn" box before you read on.

Clarify the problem

The first five minutes
Interviewer

Design a chat assistant product on top of a language model.

You

I'll assume the model already exists and I'm building the product around it: send a message, stream the reply, keep conversation history, many users. Do I own the model servers, or call someone's API?

Interviewer

You run the model servers on your own GPUs.

You

How many users, and is there a free tier and a paid tier?

Interviewer

10 million daily users, about 10 messages each. Both tiers.

You

Then GPU capacity is the scarce resource, not servers or storage. My priorities: the first words appear fast, a reply survives a dropped connection, paid users keep working when we're overloaded, nobody can use up the cluster alone, and every input and output passes a safety check. I'll leave out file uploads, tools and web search.

  • Functional: start a conversation, send a message, stream the reply as it is generated, stop a reply, list and reopen past conversations, edit a message or regenerate a reply.
  • Non-functional: first words within 1 second for most requests; replies stream at least as fast as people read; a reply survives a reconnect; no user message is ever lost; free users are throttled before paid users; every request passes safety checks.
  • Out of scope: training the model, file uploads, tool use and web search, voice.

Estimate the scale

Count in tokens, not requests: a 5-token and a 2,000-token reply are one request each but cost 400 times apart. These figures are this scenario's assumptions; measure your own model and hardware.

  • Messages: 10 million users × 10 = 100 million a day ÷ 86,400 ≈ 1,200 a second, about 3,500 at a peak of 3x.
  • Tokens per request: about 2,000 in (instructions plus history) and 500 out.
  • Output rate: 3,500 × 500 = 1.75 million tokens a second at peak, and 3,500 × 2,000 = 7 million input tokens a second to read.
  • Open streams: at 50 tokens a second per stream, a 500-token reply takes 10 seconds. Requests in flight = arrival rate × time each takes = 3,500 × 10 = 35,000 open streams at peak.
  • Storage: a user message of about 400 bytes, a reply of about 2,000 bytes and 600 bytes of metadata make 3 KB per exchange: 100 million × 3 KB = 300 GB a day, about 110 TB a year.
Estimate: How many GPU replicas at peak

A model replica (one copy of the model on its group of GPUs) runs 40 streams at once, each at about 50 tokens a second. How many replicas do you need for the peak output rate?

You know
  • 3,500 messages a second at peak
  • 500 output tokens per reply
  • One replica: 40 streams × 50 tokens a second

Show the working
  1. Peak output: 3,500 × 500 = 1,750,000 tokens a second
  2. One replica: 40 × 50 = 2,000 tokens a second
  3. 1,750,000 ÷ 2,000 = 875, call it 900; the average of 1,200 a second needs 300

Estimate 900 (anything from 300 to 3000 is a good answer)

Check with streams: 35,000 open streams ÷ 40 per replica = 875. This counts only generation; reading 7 million prompt tokens a second also takes GPU time, so plan headroom. GPUs cannot be added in seconds, which is why the deep dives are about queueing and saying no.

Estimate: Daily budget for a free user

You want a free user to manage about 10 average messages a day. How many tokens should their daily budget be, counting input and output?

You know
  • About 2,000 input tokens per message
  • About 500 output tokens per message
  • 10 messages

Show the working
  1. Per message: 2,000 + 500 = 2,500 tokens
  2. 10 × 2,500 = 25,000 tokens a day

Estimate 25 thousand (anything from 8 thousand to 80 thousand is a good answer)

Input counts because long conversations cost GPU time too. A user who keeps one huge conversation going hits the budget sooner, which is fair: they cost more.

Your turn: Define the API

Write the endpoints for sending a message and receiving the streamed reply. What happens when the connection drops halfway, and what does an overloaded server return?

Compare with a model answer

A POST answered with a server-sent event stream whose chunks have sequence ids, and a resume endpoint that takes the last id seen. A client message id makes a retried POST attach to the existing reply. Overload is 429 with Retry-After.

Define the API

HTTP API
POST /conversations                          201 { "conversation_id": "c42" }
 
POST /conversations/{id}/messages            Accept: text/event-stream
  body: { "content": "Plan three days in Lisbon",
          "client_msg_id": "m-7f3a",          retry-safe
          "parent_id": "a19" }                which reply this follows
  200 event stream:
    event: start  data: { "user_msg_id": "u20", "reply_id": "a21" }
    id: 1   event: delta  data: { "text": "Day one:" }
    id: 2   event: delta  data: { "text": " start in Alfama" }
    ...
    event: done   data: { "finish": "complete", "tokens_out": 520 }
  400 message too long
  429 Too Many Requests  Retry-After: 30  { "reason": "budget" | "busy" }
 
GET  /replies/{reply_id}/stream              Last-Event-ID: 180
  200 event stream from chunk 181, or the whole reply if it has finished
POST /replies/{reply_id}/stop                stops generation, keeps the text so far
GET  /conversations?cursor=...               newest first
GET  /conversations/{id}/messages?before=...

The decision worth arguing is server-sent events over WebSockets. Traffic is one-way while a reply streams; the user's message is an ordinary POST. Server-sent events are plain HTTP, pass through existing proxies and load balancers, and define resume: a reconnecting client sends Last-Event-ID. A WebSocket buys two-way traffic you do not need. Real-time communication compares the options.

Your turn: Model the data

Sketch the records for conversations and messages. How do you represent "regenerate this reply" and "edit my earlier message", and where do tokens live while a reply is still streaming?

Compare with a model answer

Messages form a tree: a regenerated reply or edited message is a new sibling under the same parent. Tokens in flight live in a short-lived stream buffer; the finished reply is stored once.

Model the data

Conversations, messages, usage
conversations              -- partition key: user_id
  user_id, conversation_id       PRIMARY KEY
  title          TEXT
  updated_at     TIMESTAMP       -- list newest first
  summary        TEXT NULL       -- of older turns, see deep dives
  summary_upto   message_id NULL
  current_leaf   message_id      -- the path being shown
 
messages                   -- partition key: conversation_id
  conversation_id, message_id    PRIMARY KEY   -- time-ordered ids
  parent_id      message_id NULL
  role           VARCHAR(10)     -- user | assistant
  content        TEXT
  tokens         INT
  status         VARCHAR(10)     -- streaming | complete | stopped | blocked | failed
 
usage                      -- one row per user per day
  user_id, day, tokens_in, tokens_out

Access patterns: append a message, read a conversation in order, list a user's conversations by recency. No joins, append-only writes, and 110 TB a year needs partitioning: a wide-column store (such as Cassandra) keyed by conversation fits, the same choice as in Design a Chat App. The stream buffer is separate: an in-memory log per reply (a stream in a store such as Redis), with a 10-minute expiry after the reply finishes.

Your turn: High-level design

Sketch the boxes between Maya's phone and the GPUs. Where are budgets checked, where is the context built, what decides which GPU replica gets the request, and where do tokens go on their way back?

Compare with a model answer

A gateway checks the token budget; a chat service saves the message, builds the context and runs safety checks; an inference router queues by tier and places requests on replicas with free slots; tokens return through a stream buffer.

High-level design

The chat service does the product work and owns each reply. The inference router keeps the GPUs full without letting the queue grow past what users will wait for. Model servers write tokens into a buffer; the chat service reads, checks and forwards them.

The chat service builds the request and owns the reply; the router decides which replica runs it and when; tokens return through a buffer the client can resume from.Chat apps to API gateway (POST, then SSE); API gateway to Token budgets (reserve); API gateway to Chat service; Chat service to Conversation store (history / save); Chat service to Safety classifiers (check); Chat service to Inference router (generate); Inference router to GPU model servers (batch slot); GPU model servers to Stream buffer (tokens); Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerGPU modelserversPOST, then SSEreservehistory / savecheckgeneratebatch slottokens
The chat service builds the request and owns the reply; the router decides which replica runs it and when; tokens return through a buffer the client can resume from.
One message: save, assemble, queue, stream, reconnect, and two failuresUse the arrows or ← → keys
  1. One message: save, assemble, queue, stream, reconnect, and two failures, step 1Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsused 9,000ConversationstoreChat serviceStream bufferSafetyclassifiersInference routerGPU modelservers
    budget left
    16,000

    1Maya, a free user, sends a message in conversation c42. The gateway reserves 3,000 tokens from her daily 25,000: the context estimate plus the 1,000-token reply cap.

  2. One message: save, assemble, queue, stream, reconnect, and two failures, step 2Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreu20 savedChat serviceStream bufferSafetyclassifiersInference routerGPU modelservers
    budget left
    16,000

    2The chat service saves her message first, keyed by client_msg_id, so it is never lost and a retried POST finds it.

  3. One message: save, assemble, queue, stream, reconnect, and two failures, step 3Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerGPU modelservers
    context
    2,100 tokens

    3It builds the context: system instructions, the stored summary of older turns, and the recent turns. 2,100 tokens.

  4. One message: save, assemble, queue, stream, reconnect, and two failures, step 4Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerGPU modelservers
    context
    2,100 tokens

    4A fast input classifier checks the message before any GPU time is spent. It passes.

  5. One message: save, assemble, queue, stream, reconnect, and two failures, step 5Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerwait 0.3 sGPU modelservers
    context
    2,100 tokens
    queue wait
    0.3 s

    5The router queues it as free tier. The replica that served c42 last probably still holds its prompt prefix and has a free batch slot, so it goes there.

  6. One message: save, assemble, queue, stream, reconnect, and two failures, step 6Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferchunk 42SafetyclassifiersInference routerGPU modelservers
    first words
    0.8 s
    chunks sent
    42

    6The replica reads the prompt in one pass, then generates. Numbered chunks land in the buffer; the chat service checks each sentence and forwards it. First words after about 0.8 s.

  7. One message: save, assemble, queue, stream, reconnect, and two failures, step 7Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsofflineAPI gatewayToken budgetsConversationstoreChat serviceStream bufferchunk 230SafetyclassifiersInference routerGPU modelservers
    client saw
    180
    generated
    230

    7Maya's phone switches networks at chunk 180 and the connection drops. The replica keeps writing to the buffer.

  8. One message: save, assemble, queue, stream, reconnect, and two failures, step 8Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerGPU modelservers
    client saw
    230
    generated
    230

    8The phone reconnects with Last-Event-ID: 180, possibly to another chat server. That server replays 181 to 230 from the buffer, then follows live chunks.

  9. One message: save, assemble, queue, stream, reconnect, and two failures, step 9Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstorea21 completeChat serviceStream bufferSafetyclassifiersInference routerGPU modelservers
    tokens
    2,100 in + 520 out
    budget left
    16,380

    9Done at 520 tokens. The reply is saved once, the budget is settled at 2,620 actual tokens so 380 return to Maya, and the buffer expires in 10 minutes.

  10. One message: save, assemble, queue, stream, reconnect, and two failures, step 10Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gatewayToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerresume at 301GPU modelserversreplica lost

    10Another reply: its replica dies at token 300. The router takes those 300 tokens from the buffer and sends them with the prompt, ahead of new requests, to another replica, which continues from token 301. The user sees a pause, not a restart.

  11. One message: save, assemble, queue, stream, reconnect, and two failures, step 11Chat apps to API gateway; API gateway to Token budgets; API gateway to Chat service; Chat service to Conversation store; Chat service to Safety classifiers; Chat service to Inference router; Inference router to GPU model servers; GPU model servers to Stream buffer; Chat service and Stream buffer.Chat appsAPI gateway429 free tierToken budgetsConversationstoreChat serviceStream bufferSafetyclassifiersInference routerfree wait 11 sGPU modelservers

    11Peak hour: the router reports that the free queue's expected wait has passed 10 seconds. The gateway stops admitting new free requests with 429 and a retry time; paid requests keep flowing.

1 / 11

Deep dives

Interviewers push hardest on GPU capacity, then streaming, context and safety.

Streaming that survives reconnects

The key move is to separate generating a reply from delivering it. If the reply lived only in the HTTP connection, every drop would lose a half-finished answer and the GPU time spent on it.

  • Generation writes numbered chunks into the stream buffer for that reply, whoever is listening.
  • Delivery is any chat server reading the buffer from the sequence number the client last saw.
  • Retries: a POST retried with the same client_msg_id attaches to the existing reply's stream, so one message never produces two replies (see idempotency).
  • Stop: an explicit stop frees the batch slot at once. A disconnect does not: the reply is capped at 1,000 tokens, and users expect it when they come back.

If the owning chat server crashes mid-reply, the buffer still has every chunk; another server sees the reply stuck in streaming with no heartbeat and finishes it as a continuation, like a lost replica.

GPU capacity: batching, admission and tiers

You cannot autoscale out of a peak: new GPU capacity takes minutes to start and may not exist. So the design manages a fixed pool with four mechanisms.

Continuous batching. Each step, a replica produces one token for every stream in its batch, so 40 streams cost little more per step than one. A fixed batch waits for its longest reply, leaving finished slots idle; continuous batching adds requests between steps and frees a slot the moment a reply ends. The limit is memory: each stream keeps per-token state (the attention key/value cache), so an 8,000-token conversation takes about four times the room of a 2,000-token one. Long contexts mean fewer streams per replica. A long prompt is read in pieces between generation steps so it does not stall the batch.

Admission control. The router keeps a queue per tier and estimates the wait as queued tokens ÷ pool throughput. When the free tier's estimate passes 10 seconds, the gateway rejects new free requests with 429. A fast "busy, try in 30 seconds" beats a spinner that times out after the GPU time was spent.

Rate limits counted in tokens. A per-user token bucket filled with tokens, not requests, is fair: one 8,000-token request costs what four 2,000-token ones do. The output length is unknown at the start, so reserve input plus the output cap, then refund the difference, as in the tracer.

Priority tiers. The router fills free slots from the paid queue first, with a fixed share kept for free requests so they are slowed but not starved. Under overload the free tier is shed first.

What to do when demand exceeds the GPU pool
OptionGood atBad atPick it when
Queue everythingNobody is refused.Waits grow without limit; users give up and resend, adding load.Never as the only control.
Shed by tier when the wait is too long Usual defaultLoad stays at what the pool can serve; paid users are protected; fast, clear errors.Free users see refusals at peak.The usual answer for a product with tiers.
Degrade the workEveryone gets something: shorter replies or a smaller, cheaper model.Quality drops without the user choosing it.Combined with shedding, for the free tier at peak.

Building the context

The model remembers nothing between requests: each one carries the instructions and the conversation so far, all inside a limited context window. Even when the window is large, longer prompts cost more, delay the first word, and can make answers worse (context rot). So set a budget per request, here 8,000 tokens: system instructions 1,000, the reply cap 1,000, a summary up to 500, and recent turns in the remaining 5,500.

A 40-turn conversation at about 600 tokens a turn is 24,000 tokens, so most of it cannot be sent. Older turns are covered by a rolling summary. When the recent turns pass about 4,500 tokens, well before the 5,500 limit, a background job folds the oldest 2,000 tokens of them into the summary with a small model and stores it with the id of the last message covered. Retrieval over old turns, as in Design a RAG System, can recover a detail the summary dropped; context engineering covers the prompt side.

Order matters for cost. A model server can reuse work done on a recent prompt prefix, but only if it matches token for token from the start. Put fixed instructions first, then the summary, then the turns. Between summary refreshes the cut point stays fixed and turns are only appended, so consecutive requests share a long prefix. The router sends a conversation back to the replica that served its last turn (consistent hashing on the conversation id, with a fallback when it is full) so that prefix is not read again. Caching whole replies rarely pays: two contexts almost never match after the first turn.

Safety in the path

There are two checks, placed for cost and latency (see guardrails):

  • Input, before the queue. A small classifier checks the message; a blocked request costs no GPU time. Pasted text can carry instructions aimed at the model, so treat it as data (see prompt injection).
  • Output, while streaming. Checking only the finished reply means showing unchecked text for 10 seconds; checking every token is too slow. Hold each sentence until it passes, a fraction of a second of delay. Mark passed chunks in the buffer, so a resumed stream replays only checked text. If a later sentence fails, stop generation and send a retract event so the client replaces what it showed.

If the safety service is down, fail closed: pause new generations rather than ship unchecked output. The cost: a classifier outage becomes a product outage, so it needs the same redundancy as the model servers.

Bottlenecks and failures

At 10x, 35,000 messages a second at peak, the web tier and store scale out as usual. The GPU pool needs about 9,000 replicas, which you may not be able to buy or power. The controls above decide who waits; beyond them, the levers are fewer tokens per reply, smaller models for more requests, and sending load to regions with spare capacity at night.

When this diesWhat happensWhat you do
A GPU replicaIts 40 streams stop mid-reply.The router resends each prompt plus the tokens so far, read from the buffer, to other replicas ahead of new requests.
A chat serverIts open client connections drop.Clients resume on another server; orphaned replies are finished from the buffer.
The stream bufferIn-flight chunks are lost; resume fails.Replicate it; at worst, mark those replies failed so users regenerate. Finished replies are safe.
The safety serviceNew generations pause (fail closed).Run it redundantly, like the model servers.
The conversation storeNew messages cannot be saved.Refuse new messages rather than answer and lose them; history is replicated.

What a strong answer sounds like

LevelWhat the answer does
JuniorA web server calls the model and returns the answer; history in a database; streaming once prompted.
MidStreams with server-sent events, stores conversations, limits request rates, truncates long history, and puts the model behind a queue.
SeniorCounts in tokens, sizes the GPU pool from arithmetic, separates generation from delivery for resume, explains batching and its memory limit, sheds load by tier, budgets the context with prefix reuse in mind, and places safety checks by cost.
Peak hourWeak answer
Interviewer

It's peak hour and GPUs are full. What happens?

You

Autoscale the GPU servers and put a queue in front.

Peak hourStrong answer
Interviewer

It's peak hour and GPUs are full. What happens?

You

The pool is fixed for the next few minutes, so I decide who waits. The router queues by tier and estimates the wait from queued tokens. Past 10 seconds for free users, the gateway returns 429 with a retry time, and paid users keep their share. Budgets are in tokens, so one user with huge prompts can't crowd out others.

Interviewer

Why tokens and not requests?

You

Because cost scales with tokens. An 8,000-token request uses the GPU like four 2,000-token ones, and holds four times the memory while it runs.

Follow-up questions

How do you measure whether you are meeting the latency goal?

Track time to first token and tokens per second per stream, split into queue wait and model time, per tier. Rising queue wait with flat model time means you need capacity or shedding, not faster code.

How does "regenerate" work?

It creates a sibling of the old reply with the same parent and runs the same context again. The conversation's current leaf points to the new one; the old one stays reachable. Sampling makes the new reply differ (see sampling and temperature).

How would you add memory across conversations?

Keep a small set of facts per user, extracted after conversations, and add relevant ones within the context budget. Users must be able to see and delete them (see memory).

A user deletes a conversation. What has to happen?

Delete it from the store, expire its buffer, and drop any summary or memory built from it. Logs and backups follow a retention policy you should state.

How do you roll out a new model version?

Run it on a slice of replicas, send it a small share of traffic, and compare evals, safety rates and latency before shifting more.

Self-review: LLM chat service0 of 14

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure