The request lifecycle: from arrival to first token, and streaming
A chat request crosses the network, waits in a queue, gets turned into tokens, runs one big forward pass, and then trickles out one token at a time. Knowing where each millisecond goes is the base of every other inference question. So is knowing how to stream those tokens and stop the work the moment nobody is listening.
Start here (no maths needed)
When you ask a chatbot something, your question waits its turn, gets read all at once, and then the answer is written one word at a time. The wait before the first word appears is what makes it feel fast or slow.
Think of it like this
Think of a busy burger restaurant. You place your order at the till (the request arrives). If there's a line, you wait (the queue). The cook reads your whole order and gets everything on the grill at once (reading your whole question in one go). Then the food comes out one item at a time: fries first, then the burger, then the drink (the answer, word by word). If you walk out halfway, a good kitchen stops cooking your food so the grill is free for the next person. A bad kitchen keeps cooking for an empty table.
How it works
- Your message travels over the internet to the computer running the model.
- It waits in a line if the computer is busy with other people.
- The words are chopped into small pieces called tokens, and the model reads all of them in one big step.
- The model picks the first word of the answer and sends it to you straight away.
- It keeps picking one word at a time and sending each one as soon as it's ready, until it's done or you leave.
Why robots care
A robot that hears "stop, there's a cat behind you" can't wait two seconds for the first word of its plan. If its model answers in a fixed time, say a tenth of a second, the robot can react safely. The wait before the first word is the robot's reaction time.
Try it
- With a friend, time how long it takes them to start answering "what did you eat yesterday?" and then how long the whole answer takes. Now ask a question that needs a long think first. Which part grew: the wait to start, or the talking?
- In the demo further down, push Prompt tokens up to 32,000 and watch Time to first token grow. Raise Queue wait to see a busy server. Then slide Client leaves at to 50% and untick Cancel on disconnect to see wasted work.
Words to know
- Token
- A small piece of text, often part of a word, that the model reads and writes.
- Prefill
- The first big step where the model reads your whole question at once.
- Decode
- The repeated step where the model writes one token at a time.
- TTFT
- Time to first token: how long until the first piece of the answer arrives.
- Streaming
- Sending each piece of the answer as soon as it exists, instead of all at the end.
Going deeper: everything below is the full version, with some maths. On a first read, skim the headings and the "Why it matters for robotics" box, and come back for the rest later.
Problem 1 · What happens between a request arriving and the first token leaving?
What the interviewer is testing: whether you can name every stage on the path, say which ones are usually big and which are small, and put rough numbers on them. They want to hear "prefill is compute-bound, decode is memory-bound" and "queueing dominates under load".
The concepts
Follow one chat request through a typical serving stack (an API gateway in front of an engine such as vLLM, SGLang or TensorRT-LLM):
- Network and gateway. TCP and TLS setup (skipped if the connection is reused), HTTP parsing, authentication, rate limits, request validation. A router or load balancer picks a replica, ideally one that already holds the conversation's prefix in cache (page 04).
- Templating and tokenisation. The server applies the model's chat template (system prompt, role markers), then turns text into token IDs. This runs on the CPU. It's cheap for normal prompts but not free for very long ones.
- Admission and queueing. The scheduler decides when the request joins the running batch. It needs a batch slot and enough free KV-cache memory for the prompt. Under load this wait is often the biggest part of TTFT.
- Prefix-cache lookup. If the first part of the prompt (system prompt, earlier turns) was seen recently, its keys and values are reused and that part skips computation.
- Prefill. One forward pass over all remaining prompt tokens in parallel. It writes the KV cache for every token and produces logits for the last position. Many tokens share each weight read, so prefill is compute-bound.
- Sampling. Logits go through temperature, top-p or top-k, penalties, and any grammar or JSON constraints. Out comes the first token ID. The first token comes from prefill, not from a separate decode step.
- Detokenisation and send. The token ID becomes text (carefully, because one character can span several tokens), is framed as a server-sent event, and is flushed back through every proxy to the client.
- Decode loop. Every later token is one forward pass that processes one new token per sequence but reads all the weights and the sequence's whole KV cache. With little arithmetic per byte read, decode is memory-bandwidth-bound.
The two numbers that summarise the user's experience:
TTFT = t_network + t_gateway + t_queue + t_tokenise + t_prefill + t_sample
TPOT = time per output token after the first (also called inter-token latency)
E2E = TTFT + (N_out − 1) × TPOT
Rough floors on one GPU (dense model with P parameters, T_in prompt tokens):
t_prefill ≈ 2 × P × T_in / (peak_FLOPs × MFU) compute-bound
TPOT ≥ (weight_bytes + KV_bytes_read) / mem_bandwidth memory-bound
MFU (model FLOPs utilisation) is the share of peak arithmetic you actually achieve. The factor 2 is one multiply and one add per parameter per token. Attention adds extra FLOPs that grow with T_in², small at a few thousand tokens and large at 100K.
A worked answer with real numbers
Assumptions. A Llama-3-8B-shaped model (8 billion parameters, 32 layers, 8 KV heads, head dim 128) in BF16. One NVIDIA H100 SXM. A 2,000-token prompt and a 300-token answer. An empty queue. Network round trip 40 ms (assumed; it depends on where the user is). 50% MFU in prefill (an assumption; real values vary with kernel and batch).
GPU specs (NVIDIA's H100 product page, SXM column): BF16 Tensor Core 1,979 TFLOPS with sparsity, so about 989 TFLOPS dense; memory bandwidth 3.35 TB/s; 80 GB HBM. The PCIe-class H100 NVL differs (94 GB, 3.9 TB/s, lower FLOPs), so always say which variant you mean.
Prefill FLOPs = 2 × 8e9 × 2,000 = 3.2e13 FLOPs
(+ about 2e12 for attention at 2K tokens, less with causal masking: ignore)
Effective rate = 989e12 × 0.5 ≈ 4.95e14 FLOP/s
t_prefill = 3.2e13 / 4.95e14 ≈ 65 ms
(≈ 31,000 prompt tokens/s on one GPU at this MFU)
Decode floor = 16 GB of weights / 3.35 TB/s ≈ 4.8 ms per token at batch 1
KV read = 128 KiB/token × 2,000 tokens = 250 MiB (small next to 16 GB here)
Assume TPOT ≈ 7 ms (the floor plus real-world overheads; an assumption)
TTFT ≈ 40 ms (network) + ~1 ms (tokenise, assumed) + 0 (queue) + 65 ms (prefill)
≈ 106 ms
E2E ≈ 106 ms + 299 × 7 ms ≈ 2.2 s
Now change one thing at a time and see what moves:
- 32,000-token prompt: prefill grows 16× to about 1 s (more, because attention FLOPs grow with the square of length). TTFT goes from ~0.1 s to over 1 s. This is problem 7's territory (page 03).
- Busy server: if 20 prompts like this are ahead in the queue and prefills run one after another, the wait is about 20 × 65 ms = 1.3 s. Queueing, not the model, sets TTFT.
- Repeat conversation with prefix caching: if 1,800 of the 2,000 tokens are a cached history, prefill only computes 200 tokens: about 6.5 ms. TTFT falls to about 48 ms, almost all network.
- 70B model: about 9× the FLOPs per token, so prefill on one GPU would be ~9× slower. In practice it's split over several GPUs with tensor parallelism, which adds communication per layer (see Module 08 · Distributed training).
Trade-offs
| Lever | Helps | Costs |
|---|---|---|
| Bigger decode batches | Throughput, cost per token | Higher TPOT; longer queue for new prompts if memory is full |
| Prioritise new prefills | TTFT | Running streams stall while a big prefill runs (decode hiccups) |
| Chunked prefill | Smooth TPOT, bounded stall | Slightly worse TTFT for the long prompt itself; tuning a chunk size |
| Prefix caching | TTFT and GPU work for repeated prefixes | Cache memory, routing complexity |
| More replicas closer to users | Network and queue time | Idle GPUs at off-peak; lower cache hit rates per replica |
How to say it in the interview
"The request goes through the gateway for auth and rate limiting, gets routed to a replica, templated and tokenised, then waits for a batch slot and KV memory. Then one prefill pass processes the whole prompt in parallel, writes the KV cache, and the first token is sampled from its last logits. Prefill is compute-bound: for an 8B model and a 2K prompt that's 3.2e13 FLOPs, about 65 ms on one H100 at 50% utilisation. Decode is memory-bound: each token reads all 16 GB of weights, so at 3.35 TB/s the floor is about 5 ms per token. Under real load, queueing usually dominates TTFT, so I'd measure queue time separately before touching kernels."
Common mistakes
- Forgetting the queue. A model that does 65 ms prefill can still show 3 s TTFT under load.
- Saying the first token needs a decode step. It comes out of prefill's final logits.
- Treating prefill and decode as the same kind of work. One is limited by FLOPs, the other by memory bandwidth, and the fixes differ.
- Quoting spec-sheet FLOPs without saying "with sparsity" or which variant (SXM vs PCIe/NVL).
Problem 2 · Stream tokens over SSE, handle disconnects, cancel in-flight generation
What the interviewer is testing: practical serving engineering. Do you know the wire format, the proxy traps, how to detect that a client left, and why cancelling matters for GPU capacity, not just tidiness?
The concepts
Server-sent events (SSE) is a simple one-way stream over plain HTTP. The server responds with Content-Type: text/event-stream and keeps the connection open, writing events as lines of text. Each event ends with a blank line. Lines starting with a colon are comments, handy as heartbeats. Most LLM APIs follow the OpenAI style: one JSON chunk per event and a final [DONE] marker.
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: 17
data: {"delta": "Hel"}
id: 18
data: {"delta": "lo"}
: keep-alive
data: [DONE]
Why SSE and not WebSockets? Token streaming is one-way, and SSE works through normal HTTP infrastructure (load balancers, auth, HTTP/2 multiplexing). WebSockets earn their keep when the client must also push data mid-stream, as in voice. One catch: the browser's EventSource API only does GET, so chat clients usually POST with fetch and parse the stream themselves.
Things that break streaming in practice:
- Buffering proxies. A reverse proxy or compression layer may hold bytes until a buffer fills, so the user sees nothing and then everything. Turn off response buffering for the stream route (for nginx, the
X-Accel-Buffering: noheader orproxy_buffering off) and don't gzip the event stream. - Idle timeouts. Proxies and load balancers close connections that go quiet. A long prefill or a queue wait can be silent for seconds. Send a comment line every few seconds as a heartbeat.
- Broken characters. Tokens are not characters. An emoji or a Chinese character can span two or three tokens, and decoding each token alone yields replacement characters. Detokenise incrementally: decode the running sequence, and emit only text that's complete. Engines do this for you; hand-rolled servers often get it wrong.
- Stop strings across tokens. A stop sequence like
"\nUser:"may arrive split over several tokens. Hold back a small tail of text until you know it isn't the start of a stop string.
Detecting disconnects. TCP doesn't announce that the other side vanished. The server finds out in one of three ways: the ASGI server delivers an http.disconnect message (exposed in Starlette/FastAPI as await request.is_disconnected()), a write fails, or a heartbeat write fails. That means detection lags by up to one write interval. Heartbeats during long silent phases shorten that lag.
Cancelling. Breaking out of your HTTP loop is not enough. The engine still holds the sequence in its running batch and its KV blocks in memory, and keeps decoding. You must tell the engine to abort that request ID. Engines such as vLLM expose an abort call keyed by request ID; the scheduler then drops the sequence at the next step and returns its KV blocks to the free pool.
import json, uuid
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/v1/generate")
async def generate(req: Request):
body = await req.json()
rid = uuid.uuid4().hex
async def events():
try:
async for out in engine.generate(body["prompt"], params, rid):
if await req.is_disconnected():
break
yield f"data: {json.dumps({'delta': out.delta})}\n\n"
else:
yield "data: [DONE]\n\n"
finally:
await engine.abort(rid) # frees the batch slot and KV blocks; no-op if finished
return StreamingResponse(events(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
The finally block matters most: it runs on a normal finish, a client disconnect, a server error, or task cancellation. engine stands for your engine's async client; check its docs for the exact method names.
Reconnecting. SSE has a built-in resume hook: the server tags events with id: and a reconnecting client sends Last-Event-ID. For LLMs that only works if the server keeps the generated tokens for a short time, keyed by request ID, so it can replay what was missed. Re-running the generation is not a safe substitute: with sampling, and even with greedy decoding under different batch sizes, the output can differ.
A worked answer with real numbers
Why cancellation is a capacity issue. Assume (all assumptions) 1,000 requests per minute, answers that would run to 800 tokens, and 10% of users leaving after 200 tokens (they got what they needed, or hit "stop").
Useful tokens/min = 900 × 800 + 100 × 200 = 740,000
Without abort : 100 users × 600 extra tokens = 60,000 wasted tokens/min
Waste = 60,000 / 740,000 ≈ 8% of decode capacity
KV memory held per zombie (8B shape, BF16, 2,000-token prompt + 800 output):
128 KiB × 2,800 tokens ≈ 350 MiB, held until the zombie finishes
Eight percent of a GPU fleet is real money, and zombies also take batch slots and KV memory, which pushes up queue time and TTFT for live users. With abort, waste drops to the tokens decoded between the user leaving and the server noticing: at 7 ms per token and a 40 ms round trip, a handful of tokens.
Heartbeat interval. If a proxy closes idle connections after an idle timeout (check your load balancer's setting) and a long prompt sits in the queue for 10 s, a heartbeat every 5 s keeps the stream alive and lets the server detect a gone client within about 5 s instead of only after prefill finishes.
Trade-offs
| Choice | Upside | Downside |
|---|---|---|
| Flush every token | Lowest perceived latency | More syscalls and small packets; at very high stream counts, batch 2–3 tokens per write |
| Check disconnect every token | Fast abort | A little CPU per token in the HTTP layer; checking every few tokens is usually fine |
| Replay buffer for resume | Clean reconnects on mobile networks | Memory and state on the server; needs sticky routing or shared storage |
| SSE vs WebSocket | SSE: simple, HTTP-native | No client-to-server messages mid-stream; WebSocket needed for duplex use like voice |
How to say it in the interview
"I'd return a text/event-stream response, write one JSON event per token with a final DONE marker, set no-cache, and disable proxy buffering and compression on that route. Detokenisation is incremental so multi-token characters and stop strings come out right. I'd send heartbeat comments during queueing and long prefills so proxies don't time out and so I notice dead clients. On disconnect, the generator's finally block calls the engine's abort for that request ID, which frees the batch slot and KV blocks. That matters: if 10% of users leave early, not aborting can waste around 8% of decode capacity and inflate everyone else's TTFT."
Common mistakes
- Stopping the HTTP loop but never aborting the engine request, so the GPU keeps generating for nobody.
- Decoding each token on its own and shipping broken multi-byte characters.
- Testing locally and missing a buffering proxy or CDN that only exists in production.
- Assuming a reconnect can "just regenerate" the same text.
Interactive: one request, from send to last token
Top row: what the server does (network in, queue, tokenise, prefill, then one tick per decoded token). Bottom row: what the client sees, each token arriving half a round trip later. The red line is when the client leaves. Red ticks are tokens generated for nobody.
Practise it
- Serve a small model. On a free Colab GPU or any machine with a modest GPU, start vLLM's OpenAI-compatible server with a small instruct model (0.5B–1.5B parameters). With no GPU, use llama.cpp's server on a laptop; the same measurements apply.
- Measure TTFT and TPOT from the client. Write a 30-line Python client that POSTs with
"stream": true, records the time of the first event and of every later one, and prints TTFT, mean and p95 inter-token gap, and end-to-end time. - Sweep prompt length. 100, 1K, 4K, 16K tokens. Plot TTFT against length. It should be roughly linear, bending upward at long lengths. Fit tokens/s of prefill and compare with the 2 × P × T formula and your GPU's spec.
- Add load. Fire 1, 8, 32 concurrent streams. Watch TTFT split into queue time and prefill time (vLLM's metrics endpoint reports queue time separately). TPOT should rise slowly with batch size, TTFT faster once memory is full.
- Test cancellation. Start 20 long generations, kill 10 clients after 1 s, and watch the engine's running-request count and KV-cache usage in its metrics. With a correct abort they drop within a second. Then write your own FastAPI proxy in front of the engine without the abort and watch them not drop.
- Break streaming on purpose. Put nginx in front with default buffering and see the stream arrive in clumps. Fix it with the header.
In this course
- Module 08 · Inference-aware design: the KV-cache formula and why decode is bandwidth-bound, used throughout this page.
- Module 05 · Serving: paged KV pool, prefix caching and speculative decoding in a production-scale system.
- Module 04 · Latency is a safety number: measuring wait-after-speech on a device, the robot version of TTFT.
- Module 08 · Distributed training: tensor parallelism, which large models need even to serve one request.
- Module 01 · Decision-model evals: setting thresholds and choosing metrics, the habit behind TTFT targets.
Why it matters for robotics
- TTFT is reaction time. For a robot that plans with a language model, the time from a spoken command to the first useful output is the stretch where the robot carries on doing whatever it was doing. Budget it like a control deadline, not like a web metric.
- The cloud adds a round trip you don't control. A 40 ms round trip is a good day; Wi-Fi in a warehouse or a mobile link can spike to seconds. That's the main argument for an onboard model for anything safety-related, with the cloud for slower, harder reasoning.
- Batch 1 changes the picture. An onboard model serves one robot, so there's no queue to speak of, and decode speed is set by how fast the robot's memory can stream the weights. Smaller or quantised models are the main lever there.
- Cancellation is a safety feature too. If the scene changes mid-plan, the robot should abort the stale generation at once and start a new one, the same pattern as a dropped client.
- Streaming partial output is only useful if it's actionable. Words for a human can stream; an action needs to be complete and checked before it moves a motor. Design the output so early tokens carry the decision (stop, slow, continue) and later tokens carry detail.
Self-check
Why is prefill compute-bound while decode is memory-bound?
Prefill processes many tokens per weight read, so each byte fetched is used for many multiply-adds. Decode processes one new token per sequence per step but must still read every weight and the whole KV cache, so there's little arithmetic per byte.
Estimate prefill time for a 4,000-token prompt on an 8B model, one H100 SXM, 50% MFU.
2 × 8e9 × 4,000 = 6.4e13 FLOPs. At about 989 TFLOPS dense × 0.5 ≈ 4.95e14 FLOP/s, that's about 130 ms, plus some attention cost.
A user reports 3 s TTFT, but your prefill benchmark says 100 ms. What's your first guess?
Queueing. Check scheduler queue time and KV-cache usage. If the cache is full, new requests wait for memory. Network and gateway are the next suspects.
Why isn't breaking out of the streaming loop enough on disconnect?
The engine still has the request in its batch and its KV blocks allocated, and keeps decoding. You need to call the engine's abort for that request ID to free the slot and memory.
Why do some streamed responses show garbled characters?
Each token was decoded on its own. A character can span several tokens, so you must decode incrementally and emit only complete text.
A stream works locally but arrives all at once in production. Why?
A proxy, CDN or compression layer is buffering the response. Disable buffering and compression for the stream route.
Resources
- vLLM · OpenAI-compatible server and vLLM documentation
- SGLang documentation
- TensorRT-LLM documentation
- vLLM benchmark suite (TTFT, TPOT and throughput tools)
- HTML Standard · Server-sent events and MDN · Using server-sent events
- Python asyncio · tasks and cancellation
- Orca: A Distributed Serving System for Transformer-Based Generative Models (Yu et al., OSDI 2022): iteration-level scheduling
- Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve (Agrawal et al., 2024): chunked prefill
- DistServe (Zhong et al., 2024) and Splitwise (Patel et al., 2023): why prefill and decode are split onto different GPUs
- NVIDIA H100 product page (spec table used above)