Claude API

Streaming with the Claude API: SSE, event types and mid-stream errors

TL;DR

Streaming returns results incrementally over SSE, improving UX and avoiding timeouts with large max_tokens. Use SDK helpers, group deltas by index, parse tool JSON only when the block closes, and always handle mid-stream error events.

Users stare at a blank screen for 20 seconds waiting for a long answer, or a request with a large max_tokens dies from an HTTP timeout. Both are solved with streaming: set "stream": true and the Claude API returns the result incrementally via server-sent events (SSE). Text appears as soon as the model generates it, and the connection stays alive throughout. This post explains the event flow, SDK usage and the traps in handling mid-stream errors.

Using the SDK: the simplest way

with client.messages.stream(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explain SSE in 3 sentences"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()   # full Message, with usage

Don't need incremental display? Still stream, then call get_final_message(). The docs say this is especially useful with large max_tokens, where the SDK requires streaming to avoid HTTP timeouts.

Event order

EventMeaning
message_startOpens the message, with a Message object with empty content
content_block_startStarts a block (text, tool_use, thinking) with an index
content_block_deltaEach piece of the block's content
content_block_stopCloses the block
message_deltaMessage-level updates: stop_reason, cumulative usage
message_stopEnd of stream
ping / errorKeep-alive / mid-stream error

Delta types

  • text_delta: text fragments; concatenate them for display.
  • input_json_delta: tool arguments as a partial JSON string. Accumulate until content_block_stop before parsing.
  • thinking_delta + signature_delta: when thinking is on; the signature arrives just before the block closes and verifies the thinking block.
event: content_block_delta
data: {"type":"content_block_delta","index":1,
       "delta":{"type":"input_json_delta","partial_json":"{\"location\": \"San Fra"}}

Errors mid-stream

A stream that already returned HTTP 200 can still receive an error event, for example overloaded_error under high load (equivalent to HTTP 529 without streaming). Your code must handle this; checking the initial status code isn't enough.

Risks and caveats

  • Parsing tool JSON too early: the classic bug. Parse only when the block closes, or use the SDK's accumulation helper.
  • Ignoring index: a message has several blocks; group deltas by index, not by arrival order.
  • Proxies buffering SSE: nginx or gateways can buffer and kill the streaming effect; disable buffering for this endpoint.
  • Dropped connections: what you've received may be usable, but decide whether to resend or continue; where UX allows, resending the request is simplest.
  • Unknown events: the API may add new event types; code should ignore unknown types rather than crash.

How to apply it

  • Dev: Default to messages.stream() for long requests; log get_final_message() to get accurate usage.
  • Tech Lead: Write a single adapter layer for SSE, mid-stream errors and retries, instead of each service doing its own.
  • PM: Measure "time to first token" rather than total time; it's the metric users feel most.

Sources & references

  1. Streaming messages — Claude Docs