Claude Certification Guides
CCDV-F/Domain 29 min read

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
EventFiresCarries
message_startOnce, firstThe Message shell with id, model, input token usage
content_block_startPer content blockThe block type and index
content_block_deltaMany times per blockA delta of the block's content
content_block_stopPer content blockThe index that finished
message_deltaOnce, near the endstop_reason, stop_sequence, cumulative output usage
message_stopOnce, lastNothing further
pingOccasionallyKeep-alive; ignore it
errorOn failure mid-streamAn 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_delta carries a text fragment. Concatenate fragments for the same index.
  • input_json_delta carries partial_json, a fragment of a tool call's input. Accumulate the fragments for that index and parse the JSON only after content_block_stop. Individual fragments are not valid JSON.
  • thinking_delta carries a fragment of an extended thinking block, with a separate signature_delta arriving 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: true or the SDK stream() helper; get_final_message() / finalMessage() returns the complete assembled message.
  • Event order is message_start, then per-block content_block_start / content_block_delta / content_block_stop, then message_delta, then message_stop.
  • text_delta carries text; input_json_delta carries partial_json fragments of tool input that must be concatenated before parsing.
  • stop_reason and final output usage arrive in message_delta, not message_start.
  • ping events are keep-alives; error events can arrive mid-stream and leave you with a partial response.
  • Large max_tokens values require streaming to avoid HTTP timeouts; the SDKs enforce this.

Check yourself

5 questions

  1. 1.When a request is made with stream: true, which server-sent event arrives first?

  2. 2.While consuming a stream manually, a developer needs the final stop_reason and output token count. In which event are they delivered?

  3. 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. 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. 5.Why do Anthropic's docs recommend streaming for long generations?