Claude API Streaming: How the Event Stream Works and How to Consume It
What streaming actually changes
A non-streaming request to the Claude API holds the connection open until the model has finished writing, then returns one JSON object. A streaming request returns the same content, but as a sequence of small events that arrive while generation is still happening.
The difference matters in two places. Interactive interfaces feel responsive because the first characters appear in a few hundred milliseconds instead of after the full response. And agent workloads — coding assistants, tool-calling loops, anything that runs long — need incremental output to show progress and to abort early when the answer is already wrong.
Streaming does not make generation faster. The total time to the last token is roughly the same. What changes is time-to-first-token, and what you can do with the response before it is complete.
The transport: server-sent events
Set stream to true in the request body and the response arrives as a server-sent event stream with content type text/event-stream.
Each event is two lines followed by a blank line: an event: line naming the type, and a data: line carrying a JSON object. The blank line is the delimiter — a parser that splits on newlines alone will corrupt multi-line payloads.
Two properties of SSE are worth internalising before you write a parser by hand. Events can be split across TCP packets, so you must buffer until you see the blank-line terminator. And the stream carries periodic ping events that contain nothing useful; they exist to keep intermediaries from closing an idle connection, and your parser must ignore them rather than treat them as malformed.
The official SDKs handle all of this. Hand-rolled parsers are where most streaming bugs live.
The event sequence
Anthropic's stream is not a flat sequence of text fragments. It is a structured envelope, and understanding the shape is what lets you handle tool calls and stop reasons correctly.
|
Event |
When it arrives |
What it carries |
|---|---|---|
|
|
Once, first |
The message shell: id, model, role, and initial usage with input tokens |
|
|
Once per content block |
The block index and its type — |
|
|
Many times per block |
The incremental payload — this is the actual content |
|
|
Once per content block |
Signals that block index is complete |
|
|
Once, near the end |
Top-level changes: |
|
|
Once, last |
The stream is finished |
|
|
Any time |
Nothing — ignore it |
|
|
Any time |
An error object; the stream ends after this |
The nesting is the part people miss. A response can contain several content blocks, each with its own index. Text and tool calls are different block types in the same stream, and their deltas interleave by index, not by arrival order alone.
Accumulating deltas
For a text block, each content_block_delta carries a text_delta with a text field. Reconstructing the full response means appending those fragments in order, keyed by block index.
The naive version — concatenating every delta into a single string — works only when the response has exactly one text block and no tool calls. The moment a tool call appears, that approach silently mixes JSON fragments into your prose.
Keep a map from block index to accumulated content. On content_block_start, create the entry with the block's type. On content_block_delta, append to the entry at that index. On content_block_stop, mark it final. This is what the SDKs do internally, and it is the only version that survives contact with tool use.
Tool calls in a stream
When the model calls a tool, the content block has type tool_use and its deltas are input_json_delta — fragments of a JSON string, not parsed objects.
These fragments are not individually valid JSON. A tool input of {"city": "Tokyo"} might arrive as {"ci, then ty": "To, then kyo"}. You must concatenate the whole string and parse once, after content_block_stop for that index.
Code that tries to parse each delta will throw on nearly every call. This is the single most common streaming bug in tool-using agents, and it is easy to miss in testing because short tool inputs sometimes arrive in one piece.
Stop reasons and usage
The message_delta event near the end carries stop_reason, and it is the only place you get it. The values you will actually branch on: end_turn for a natural finish, max_tokens when the response was truncated by your own limit, tool_use when the model is waiting for you to run a tool and return the result, and stop_sequence when a sequence you supplied was hit.
Treating a truncated response as a complete one is a real failure mode. If you build anything that parses the model's output as structured data, check stop_reason before parsing — max_tokens means what you have is a fragment.
Usage arrives split across two events: input tokens in message_start, output tokens in message_delta. Cost attribution that reads only one of them will be wrong. See Claude API pricing for what drives the bill beyond raw token counts.
Failure modes worth handling
An error event can arrive mid-stream, after you have already received usable text. Overload and rate-limit conditions surface this way rather than as an HTTP status, because the status line was sent at 200 before generation began. Your handler needs a path for partial-then-failed, not just success or failure.
A stalled stream is different from a slow one. If no event — not even a ping — has arrived for longer than your tolerance, the connection is likely dead upstream and will never recover. An idle timeout that resets on every event, including pings, is the right shape.
Retrying a stream is not free. If you retry after receiving partial output, you either discard tokens you already paid for or risk duplicating them in your transcript. Decide which before it happens in production.
Streaming through a proxy or gateway
If your requests pass through a relay, streaming is the capability most likely to be quietly broken. A relay that buffers the upstream response and releases it at the end returns byte-identical content while destroying the entire benefit — and it passes any test that only checks the final text.
Verify with a stopwatch, not an assertion. Measure the wall-clock time to the first visible chunk against a direct call. If the two are the same, the stream is being buffered somewhere in the path.
The other failure is tool-call mangling: relays that re-serialise events can reorder block indices or merge input_json_delta fragments incorrectly. Test a tool-calling request through the path, not just a plain completion. Claude API proxy covers the broader evaluation checklist for relays.
FAQ
Does streaming cost more than a single request? No. Billing is by token, and the token counts are the same. Streaming changes delivery, not consumption.
Can I stream and use tools at the same time? Yes. Tool calls appear as tool_use content blocks whose deltas are partial JSON. Accumulate and parse once per block.
Why do I see empty events? Those are ping events. They keep intermediaries from timing out an idle connection and carry no content. Ignore them.
Is the streamed output identical to the non-streamed output? Yes for the content. The difference is the envelope: streaming splits the same message into events, and puts stop_reason and output usage in message_delta instead of the top-level response object.
How do I know the response was complete? Check stop_reason in message_delta. end_turn means the model finished on its own; max_tokens means it was cut off by your limit.
ROIBest AI exposes an OpenAI-compatible endpoint alongside Anthropic-format access, so existing streaming clients keep working by changing the base URL. See what an OpenAI-compatible API is if you are deciding which protocol to target.