ChatGPT style assistant0/66 done
Part 3 · System design · Real products

ChatGPT style assistant

Users chat with an AI model. Answers stream word by word, conversations are saved, and the service runs on expensive, limited GPUs.

In plain words

You ask a question and the answer starts typing itself out almost at once. The model really does make the answer one word at a time, and the app shows each word as it arrives, so you aren't staring at a spinner for 10 seconds.

The model can only read so much at once. If your chat has gone on for an hour, the app keeps the instructions and your most recent messages, and leaves out the oldest ones, like a friend who remembers the last half hour of a conversation but not the start. Behind all of it are GPUs, which are expensive and limited, so requests wait in a queue and are run together in batches.

Examples

  • Streaming uses server sent events: one request, many small chunks back.
  • Limits are counted in tokens, not requests: a 5,000 word question costs more than a hello.
  • Easy questions can go to a smaller, cheaper model.
  • Close the tab and generation stops, so no GPU time is wasted.

1. Requirements

Functional

Non functional

Questions to ask

2. Estimates

messages100M a day, about 1.2k per second, peaks 3x
tokensabout 1k tokens in and 500 out per message
GPU timea few seconds per answer; GPUs are the bottleneck and the bill

3. API

POST /conversations/{id}/messages { text }     stream of tokens (server sent events)
GET  /conversations?cursor=...                 [conversation titles]
GET  /conversations/{id}                       messages
POST /conversations/{id}/stop                  stops generating

4. Data model

conversations(id, user_id, title, created_at)
messages(conversation_id, seq, role, text, tokens)
usage(user_id, day, tokens)   for limits and billing
files: object storage; embeddings in a vector index for retrieval

5. High level design

Follow one request at a time, top to bottom.

Send a message

  1. BrowserPOST message, keeps the stream open
  2. API serverauth, token based rate limit by tier
  3. Context buildersystem prompt + newest messages that fit the budget
  4. Queueper tier priority
  5. Model servers (GPU)batch requests, generate token by token
  6. Browsershows tokens as server sent events arrive

Save

  1. API serverwrites the user message and the final answer with seq numbers
  2. Usage tabletokens per user per day
  3. Title jobnames the conversation in the background

6. Deep dives

Why stream?

A full answer takes seconds. Streaming tokens as they are made shows the first word in under a second. Use server sent events: one request, many small chunks.

The conversation is longer than the context window?

Keep the system prompt and the newest messages that fit the token budget, and drop or summarise older ones. Long term facts go in a separate memory store.

How do you share scarce GPUs?

A queue in front of the model servers, per tier priority, and batching: the server runs many requests together. Rate limit by tokens per user, not just requests.

How do you keep costs down?

Cache answers to identical prompts, route easy questions to a smaller model, and cache the shared system prompt prefix on the GPU.

See the core idea run

Core logic (JavaScript, tested)

// Fit a conversation into the model's context window: keep the system prompt and the newest
// messages, drop the oldest middle ones. Tokens are estimated as about 4 characters each.
const tokens = s => Math.ceil(s.length / 4);

function fitContext(system, messages, budget) {
  let used = tokens(system);
  const kept = [];
  for (let i = messages.length - 1; i >= 0; i--) {
    const t = tokens(messages[i].text);
    if (used + t > budget) break;
    used += t;
    kept.unshift(messages[i]);
  }
  return { kept, dropped: messages.length - kept.length, used };
}

// Stream the answer: the model makes tokens one by one; the client shows each as it arrives.
async function* streamTokens(text) {
  for (const word of text.split(" ")) yield word;
}
async function collect(stream) {
  const parts = [];
  for await (const t of stream) parts.push(t);
  return parts.join(" ");
}

7. Failures

Model server crashes mid answerThe client sees the stream end; offer "regenerate". Save partial answers as partial.
GPU capacity fullQueue with a visible wait, fall back to a smaller model for free users, or return 429 with retry time.
User closes the tabStop generating to save GPU time, unless it is a background task.

8. Trade-offs

Wrap up

Say out loud: The app servers are cheap; GPUs are the bottleneck. Requests go through a token based rate limiter into a queue, model servers batch them, and tokens stream back over server sent events. Each turn sends the system prompt plus the newest messages that fit the context budget. Conversations are stored per user with a sequence number.