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
| Event | Meaning |
|---|---|
message_start | Opens the message, with a Message object with empty content |
content_block_start | Starts a block (text, tool_use, thinking) with an index |
content_block_delta | Each piece of the block's content |
content_block_stop | Closes the block |
message_delta | Message-level updates: stop_reason, cumulative usage |
message_stop | End of stream |
ping / error | Keep-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 untilcontent_block_stopbefore 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 byindex, 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; logget_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
- Streaming messages — Claude Docs