Study guides / CCDV-F / Domain 1

Applications & Integration · Lesson 2 of 11

1.2 - Streaming Responses

Handle server-sent event streams correctly, including the deltas that build up tool_use input incrementally.

Setting stream: true turns a single JSON response into a sequence of server-sent events. The full lifecycle is: one message_start event (carrying an initial, mostly-empty message object with a starting usage), then one content_block_start / a run of content_block_delta / content_block_stop cycle per content block, then a top-level message_delta event (carrying the final stop_reason, any stop_sequence, and cumulative output usage), and finally message_stop. Periodic ping events may appear anywhere in the stream as keep-alives and should simply be ignored. The point of streaming is perceived latency — the user sees the first tokens almost immediately instead of waiting for the whole response — not a change in what the model computes or how much it costs.

Delta types differ by block

A content_block_delta event's shape depends on the type of block it belongs to: a text block streams text_delta events, each with a small chunk of the growing string; a tool_use block streams input_json_delta events, each carrying a partial_json fragment; a thinking block streams thinking_delta events and, at the very end, a single signature_delta event that carries a cryptographic signature over the completed thinking content. Each content_block_start event tells you the index and type of the block that's about to stream, which matters the moment a response has more than one block in flight conceptually (they still arrive in order, but tracking index keeps multi-block accumulation unambiguous).

Tool use over a stream

When Claude streams a tool call, the arguments arrive as a series of input_json_delta events containing partial JSON fragments, not one clean object. Your code must concatenate these fragments into a per-block string buffer and parse the accumulated string only once content_block_stop fires for that block — parsing a partial fragment as JSON on every delta will throw on most of them, since a fragment like {"query": "par is not valid JSON on its own. Most official SDKs expose a higher-level stream helper that accumulates this for you and emits a fully-formed message at the end; using the raw event stream directly is what the exam expects you to be able to reason about even if your actual code uses the helper.

Common exam distractor

An option that says a stream can be inspected for a final, reliable stop_reason on every delta is a trap. Early in the stream stop_reason is null in the message_start payload; it only becomes final and trustworthy on the message_delta event near the end of the stream. Reading it from an earlier event and acting on it is reading an incomplete value.

Errors mid-stream

A stream can also emit an error event after generation has already begun — for example an overloaded_error partway through. A client that only checks the HTTP status code at connection time misses this: the initial connection can return 200 and start streaming normally, then fail partway through with a distinct SSE error event. Robust streaming code watches for the error event type throughout the stream, not just at connection time, and is prepared to retry the whole request from scratch since there's no way to resume a partially-streamed generation.

The SDK's stream helper vs the raw event iterator

Official SDKs provide a higher-level streaming helper that emits parsed, semantic events (a callback per text delta, a callback per completed content block) and exposes a method to await the fully accumulated final message once the stream ends, so most application code never has to hand-roll the delta-accumulation logic described above. Reaching for the raw SSE event iterator directly is still worth knowing how to do — some event details, like the exact block index or a raw ping, aren't always surfaced by the convenience layer, and in a language without an official SDK the raw lifecycle is all you have. Understanding the raw event sequence is also what lets you reason correctly about what the helper is doing under the hood, which is the version of streaming knowledge the exam actually tests, even if your production code uses the helper.

Usage accounting during a stream

input_tokens is known before generation starts and appears in the initial message_start snapshot, since the full input has already been processed by the time any output streams back. output_tokens, by contrast, only reaches its final value on message_delta, because generation isn't complete until then — reading it from an earlier point in the stream gets a partial or stale count. Cost itself does not change because a request was streamed: streaming affects only how the response is delivered to the client, not how many tokens are processed or produced, so there is no price premium or discount tied to stream: true versus an equivalent blocking call.

Key concept

A dropped connection mid-stream is not resumable from where it left off — there is no cursor or offset you can hand back to the API to continue a partial generation. The correct recovery is a fresh request from scratch, which is also why idempotency and retry discipline (Lesson 1.8) matter even more for streaming clients than for simple blocking calls.

Exam traps

Practice question

An application needs to show the user a typing-style effect for Claude's answer while it's still being generated. Which capability is the correct fit?

  • A Set a low max_tokens value so the response finishes faster.

    A lower cap shortens the answer; it does not deliver partial output as it's produced.

  • B Poll the same request repeatedly until it completes.

    The Messages API has no polling mechanism for an in-flight non-streamed request.

  • C Send the request with stream set to true and render each text_delta as it arrives. Correct

    Streaming is exactly the mechanism for incremental delivery of output as the model generates it.

  • D Use prompt caching to speed up generation.

    Caching reduces re-processing of repeated input; it doesn't change whether output arrives incrementally or all at once.

Build exercise: Stream a response and correctly reassemble a streamed tool call

Intermediate · 35 minutes

You'll practice:

  1. Send a streamed request with a simple text prompt and print the event type of every SSE event you receive, in order.

    Seeing the raw event sequence once - message_start, content_block_start, a run of deltas, content_block_stop, message_delta, message_stop - makes the rest of this exercise concrete instead of abstract.

    You should see: A printed list of event types matching the documented lifecycle, with several text_delta events between one content_block_start and content_block_stop.

    Hints
    1. What event type would you expect to appear many times in a row for a multi-sentence answer?
    2. text_delta events repeat once per chunk of generated text within a single content_block_start/content_block_stop pair - count them to see how granular the chunks are.
    3. for event in client.messages.stream(model="claude-sonnet-5", max_tokens=300, messages=[{"role":"user","content":"Explain SSE briefly."}]):
          print(event.type)
  2. Send a streamed request that gives Claude one tool to call, with a prompt that guarantees it will call that tool.

    Text-only streams are straightforward; tool-call streams are where reassembly bugs actually happen.

    You should see: A sequence of input_json_delta events whose partial_json fields, concatenated in order, form one valid JSON object once content_block_stop fires.

    Hints
    1. How should you key your buffer if you eventually want to support a response with more than one tool_use block?
    2. Concatenate the partial_json strings into a buffer per content block index. Only attempt JSON parsing on the buffer once you see that block's content_block_stop.
    3. let buffers = {};
      for await (const event of stream) {
        if (event.type === "content_block_start") buffers[event.index] = "";
        if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
          buffers[event.index] += event.delta.partial_json;
        }
        if (event.type === "content_block_stop") {
          const parsed = JSON.parse(buffers[event.index]);
          console.log(parsed);
        }
      }
  3. Read stop_reason and cumulative usage only from the message_delta event, and print them alongside a note of which earlier events would have given you an incomplete or null value if you'd read it there instead.

    This makes the message_start-vs-message_delta trap concrete rather than a rule to memorise.

    You should see: message_start shows stop_reason as null; message_delta shows the real final value plus cumulative output_tokens.

    Hints
    1. Which single event in the whole stream is documented as carrying the final stop_reason?
    2. message_start's nested message object has stop_reason: null by design - it hasn't happened yet. message_delta.delta.stop_reason is the trustworthy one.
    3. if (event.type === "message_delta") {
        console.log("final stop_reason:", event.delta.stop_reason);
        console.log("output_tokens:", event.usage.output_tokens);
      }
  4. Simulate or intentionally trigger a mid-stream failure (e.g. by deliberately sending an oversized or malformed request that starts streaming before failing, or by catching a network interruption) and confirm your code checks for an error event type, not just the initial connection status.

    This is the scenario a naive streaming client silently mishandles - a stream that started fine but died partway through.

    You should see: Your consumer loop explicitly checks for event.type === "error" during iteration, separate from any initial connection error handling, and triggers a full retry rather than trying to resume.

    Hints
    1. If a stream fails at token 400 of an expected 800, is there a way to resume from token 400, or do you have to start over?
    2. There is no resume mechanism for a partially-streamed generation - on an error event mid-stream, discard the partial buffer and retry the whole request.
    3. for await (const event of stream) {
        if (event.type === "error") {
          console.error("stream failed mid-generation:", event.error);
          throw new Error("retry-from-scratch");
        }
        // ... normal handling
      }

Sources