How does streaming work in LLM APIs and why does it matter for UX?
1.1k views
17 of 83
1
The problem
Question presented to candidate:
"Most LLM APIs let you set stream: true. Explain what actually happens on the wire when you do, why product teams almost always want it for chat, and what it costs you in engineering effort."
What a strong answer should cover:
The model generates one token at a time anyway; streaming forwards each token (or small group) as it is produced over a long-lived HTTP response using server-sent events, instead of holding everything until the end
The UX win is perceived latency: time to first token (TTFT) replaces total generation time as the wait the user feels, while total time, token count and cost stay the same
The provider event vocabulary: typed events such as Anthropic message_start / content_block_delta / message_delta / message_stop, or OpenAI Responses response.output_text.delta / response.completed, plus tool-call arguments arriving as partial JSON that only parses at the end
The engineering cost: mid-stream errors after a 200 status, usage that arrives last (and may be missing if the stream is cut), cancellation that must reach the provider, and proxies or CDNs that buffer and silently turn a stream back into a blocking response
When not to stream: output consumed by code rather than people, batch jobs, very short answers, and cases where every word must pass a safety check before anyone sees it
Code / implementation expected: Optional โ reading a stream with an SDK, or parsing the SSE events and assembling text and tool input by hand.
streaming
sse
latency
ux
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Engineers building a chat or assistant feature who want to know what stream: true really changes, and what it breaks.
Difficulty: Medium
Verified with: Node 22.22.0 and Python 3.12.3 (both examples below, identical output). Provider event names were checked in October 2026 against the Anthropic streaming docs (platform.claude.com, "Streaming messages") and the type definitions of the official openai npm package (v7.36.0).
How to read this doc: Every idea is explained in plain language first, then tagged with ๐ Interview term: so you also learn the words an interviewer expects. All program output shown was produced by actually running the examples; the Verified section quotes it in full.
1. Why This Even Matters โ A Story First
You ask a friend a question by text message. Version one: they think for ten seconds, typing the whole reply, a
// Blocking vs streaming on a fake clock: when does the user first see text,// what does a buffering proxy do to that, and what does cancellation save?// All times are simulated milliseconds; nothing here calls a real model.constTTFT_MS = 700; // queueing + prefill before the first output tokenconstMS_PER_TOKEN = 30; // decode speed: about 33 tokens per secondconstANSWER_TOKENS = 300; // length of the full answerconstCHARS_PER_TOKEN = 4; // rough English averageconstSSE_OVERHEAD = 60; // bytes of "data: {...}" framing around each delta// Time at which output token i (0-based) leaves the model.consttokenAt = (i) => TTFT_MS + i * MS_PER_TOKEN;
const lastTokenAt = tokenAt(ANSWER_TOKENS - 1);
// A proxy that holds bytes until its buffer is full (or the response ends).functionfirstFlushThroughBuffer(bufferBytes) {
let held = 0;
for (let i = 0; i < ANSWER_TOKENS; i++) {
held += CHARS_PER_TOKEN + SSE_OVERHEAD;
if (held >= bufferBytes) return { at: tokenAt(i), tokens: i + 1 };
}
return { at: lastTokenAt, tokens: ANSWER_TOKENS };
}
constpad = (s, n) => String(s).padEnd(n);
constrow = (name, first, full, note) =>
console.log(pad(name, 36) + pad(first + " ms", 12) + pad(full + " ms", 12) + note);
console.log("Answer: " + ANSWER_TOKENS + " tokens, TTFT " + TTFT_MS + " ms, " + MS_PER_TOKEN + " ms/token");
console.log("");
console.log(pad("delivery", 36) + pad("first text", 12) + pad("complete", 12) + "note");
row("blocking (stream: false)", lastTokenAt, lastTokenAt, "spinner the whole time");
row("streaming, direct", tokenAt(0), lastTokenAt, "first token shown at once");
const f4 = firstFlushThroughBuffer(4096);
row("streaming via 4 KB buffering proxy", f4.at, lastTokenAt, "first flush carries " + f4.tokens + " tokens");
row("streaming via full-buffer proxy", lastTokenAt, lastTokenAt, "same as blocking");
console.log("");
console.log("Same total time in every row: streaming changes when text appears, not when it ends.");
console.log("Share of the wait spent before first text (direct streaming): " +
(100 * tokenAt(0) / lastTokenAt).toFixed(1) + "%");
// The user reads the first part and presses Stop at t = 3000 ms.constSTOP_AT = 3000;
const generatedByStop = Math.floor((STOP_AT - TTFT_MS) / MS_PER_TOKEN) + 1;
console.log("");
console.log("User presses Stop at " + STOP_AT + " ms:");
console.log(" abort propagated to the provider -> " + generatedByStop + " output tokens generated");
console.log(" abort ignored (server keeps reading) -> " + ANSWER_TOKENS + " output tokens generated");
console.log(" wasted by ignoring it: " + (ANSWER_TOKENS - generatedByStop) + " tokens (" +
(100 * (ANSWER_TOKENS - generatedByStop) / ANSWER_TOKENS).toFixed(1) + "% of the answer)");