Lesson 2.4
Streaming Responses
The server-sent event sequence the Messages API emits, how to consume it with the SDK helpers, and why streaming is mandatory for long outputs.
Streaming returns the response incrementally as server-sent events (SSE) instead of one JSON body at the end. Users see text appear immediately, and long generations no longer collide with HTTP timeouts. The exam tests the exact event order, which delta types exist, and how the SDK helpers simplify the work.
Turning it on
Set "stream": true in the request body, or use the SDK's stream() helper, which sets the flag and parses events for you.
with client.messages.stream(
model=MODEL, max_tokens=4096,
messages=[{"role": "user", "content": "Write a short story about a lighthouse."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
print(final.usage.output_tokens)
const stream = client.messages.stream({
model: MODEL, max_tokens: 4096,
messages: [{ role: "user", content: "Write a short story about a lighthouse." }],
});
stream.on("text", (delta) => process.stdout.write(delta));
const final = await stream.finalMessage();
console.log(final.usage.output_tokens);
get_final_message() / finalMessage() returns the fully assembled Message object with content, stop_reason and usage, so you can stream to the user and still process the complete response afterwards.
The event sequence
Every streamed response follows the same skeleton:
event: message_start {"message": {"id": "...", "usage": {"input_tokens": 25, ...}}}
event: content_block_start {"index": 0, "content_block": {"type": "text", "text": ""}}
event: content_block_delta {"index": 0, "delta": {"type": "text_delta", "text": "Once"}}
event: content_block_delta {"index": 0, "delta": {"type": "text_delta", "text": " upon"}}
event: content_block_stop {"index": 0}
event: message_delta {"delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 212}}
event: message_stop
| Event | Fires | Carries |
|---|---|---|
message_start | Once, first | The Message shell with id, model, input token usage |
content_block_start | Per content block | The block type and index |
content_block_delta | Many times per block | A delta of the block's content |
content_block_stop | Per content block | The index that finished |
message_delta | Once, near the end | stop_reason, stop_sequence, cumulative output usage |
message_stop | Once, last | Nothing further |
ping | Occasionally | Keep-alive; ignore it |
error | On failure mid-stream | An error object such as overloaded_error |
Blocks are identified by index. A response with text followed by a tool call will have index 0 for the text block and index 1 for the tool_use block, each with its own start, deltas and stop.
Delta types
The delta.type inside content_block_delta tells you what kind of content is arriving:
text_deltacarries atextfragment. Concatenate fragments for the same index.input_json_deltacarriespartial_json, a fragment of a tool call'sinput. Accumulate the fragments for that index and parse the JSON only aftercontent_block_stop. Individual fragments are not valid JSON.thinking_deltacarries a fragment of an extended thinking block, with a separatesignature_deltaarriving at the end of the block.
Final usage comes in message_delta, not message_start; the start event only knows the input side.
Streaming with tools
The stop reason still arrives in message_delta, so a streaming agent loop reads it after the stream ends, then runs tools and calls again exactly as in the non-streaming loop. The SDK's accumulated message already contains parsed tool_use blocks. By default the API buffers each tool-call parameter and emits it when complete; the eager_input_streaming tool option streams large inputs as they are generated, at the cost of making the client responsible for validating the parsed JSON before acting on it.
Why streaming is not optional for long outputs
Non-streaming requests hold one HTTP connection open until the whole response is ready. The SDKs refuse or warn when you request a very large max_tokens without streaming, because the request would be likely to time out. The rule of thumb: if output could take more than a minute or two, stream. Streaming also lets you measure time-to-first-token separately from total latency, which is the metric that drives perceived responsiveness.
Handling interruptions
A stream can end early with an error event or a dropped connection. Treat what you have received as partial, surface a retry, and do not append the partial assistant turn to conversation history as if it were complete. The SDK helpers expose the exception from the iterator or the finalMessage() promise, so wrap the loop in the usual typed error handling.
Raw HTTP
Without an SDK, set Accept: text/event-stream, read lines, and parse each data: payload as JSON keyed by the preceding event: line. Most languages have an SSE parser; writing one by hand is error-prone because events may be split across TCP packets.
Exam focus
- Streaming is enabled with
stream: trueor the SDKstream()helper;get_final_message()/finalMessage()returns the complete assembled message. - Event order is
message_start, then per-blockcontent_block_start/content_block_delta/content_block_stop, thenmessage_delta, thenmessage_stop. text_deltacarries text;input_json_deltacarriespartial_jsonfragments of tool input that must be concatenated before parsing.stop_reasonand final outputusagearrive inmessage_delta, notmessage_start.pingevents are keep-alives;errorevents can arrive mid-stream and leave you with a partial response.- Large
max_tokensvalues require streaming to avoid HTTP timeouts; the SDKs enforce this.
Check yourself
5 questions
1.When a request is made with stream: true, which server-sent event arrives first?
2.While consuming a stream manually, a developer needs the final stop_reason and output token count. In which event are they delivered?
3.A streaming agent accumulates text with: if (ev.type === "content_block_delta") buffer += ev.delta.text; When Claude calls a tool, buffer receives "undefined" and the tool input is never captured. Why?
4.Using the TypeScript SDK: const stream = client.messages.stream({ model, max_tokens: 2000, messages }); stream.on("text", (t) => process.stdout.write(t)); const final = await stream.finalMessage(); What does finalMessage() resolve to?
5.Why do Anthropic's docs recommend streaming for long generations?
