How Server-Sent Events Power Claude's Streaming Responses
When you call the Claude Messages API without streaming, you send a request and wait.
Nothing comes back until the entire response is finished generating.
For a long answer, that wait can be several seconds of a blank screen.
Streaming changes the delivery mechanism, not the content: instead of one big response at the end, Claude sends the same response as a sequence of small, typed events over a single open connection, as the words are generated.
Understanding the transport underneath streaming - Server-Sent Events - and the shape of the events it carries is the foundation for everything else in this section, from printing tokens as they arrive to building a live chat UI to streaming tool calls.
Summary
- Core Idea: Claude's streaming API sends the response as an ordered sequence of typed SSE events over one HTTP connection, rather than as a single JSON blob at the end.
- Why It Matters: It lets an application start rendering output the moment the first tokens exist, instead of blocking the user interface until generation completes.
- Key Concepts: Server-Sent Events, event stream,
message_start,content_block_delta,text_delta,message_delta,message_stop. - When to Use: Any user-facing surface where perceived latency matters - chat UIs, coding assistants, live document generation, and any long response the user is actively watching.
- Limitations / Trade-offs: Streaming adds client-side complexity (partial state, error handling, reconnects) and is not compatible with some caching or batch-processing patterns that want the full response as a unit.
- Related Topics: the Messages API request/response model, tool use, extended thinking, the Python SDK's streaming helper.
Foundations
Server-Sent Events (SSE) is a long-standing web standard for one-directional, server-to-client streaming over plain HTTP.
A client opens a normal HTTP request, but instead of the server sending one response and closing the connection, the server keeps the connection open and writes a sequence of small text frames to it over time.
Each frame is prefixed with data: and separated by blank lines - it is deliberately simple, simpler than WebSockets, because it only needs to flow in one direction: server to client.
Claude's Messages API uses this exact mechanism when you request stream=true (or use the SDK's streaming helper).
Instead of one HTTP response body containing the finished message, the connection stays open and the server writes a series of events, each one a small JSON object describing one incremental piece of the response.
A useful analogy: a non-streaming response is like waiting for a whole letter to be printed and then handed to you.
A streaming response is like watching someone type that same letter in front of you, one line at a time, through a window that tells you exactly what kind of thing just got typed - a heading, a sentence, a signature.
The event types are what make that useful: each one names precisely what changed, so client code never has to guess.
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain SSE in one paragraph."}],
) as stream:
for event in stream:
print(event.type) # message_start, content_block_delta, ..., message_stopThat loop is the shape every streaming consumer takes: open a stream, receive a sequence of typed events, and react to each type differently.
Mechanics & Interactions
The event sequence for a typical text response follows a fixed skeleton:
message_start
content_block_start (index 0, type: text)
content_block_delta (text_delta, "Hello")
content_block_delta (text_delta, " there")
content_block_delta (text_delta, "!")
content_block_stop (index 0)
message_delta (stop_reason, usage totals)
message_stop
message_start arrives first and carries the message's metadata shell - its id, model, and initial (near-zero) usage - before any content exists yet.
Between that and message_stop, the response is built out of one or more content blocks, each opened with content_block_start and closed with content_block_stop.
A single response can contain multiple content blocks in sequence - for example a thinking block followed by a text block, or a text block followed by a tool_use block - and each is identified by its index so a client can track several blocks' partial state independently.
The actual token-by-token output arrives as content_block_delta events nested inside a block's start/stop pair.
For a text block, the delta's delta.type is text_delta and its delta.text is the next fragment of text - concatenating every text_delta for a block, in order, reconstructs that block's full text.
content_block_delta is a generic envelope, though - the delta.type inside it varies by what kind of block is streaming: text_delta for prose, input_json_delta for a tool call's arguments, and thinking_delta for extended thinking, each carrying a different partial payload that gets assembled differently.
message_delta arrives once, near the end, and carries message-level fields that were not known until generation finished - stop_reason (why the model stopped) and running token usage totals.
message_stop is the final event and signals that the stream is complete; no more events will follow for this response.
A common reasoning mistake is expecting one content_block_delta to equal one token, or one text_delta to be a clean word boundary.
Neither is guaranteed - a delta can be a partial word, several tokens, or even split mid-character-sequence for multibyte text - so client code should treat deltas as opaque fragments to concatenate, never as units to parse individually.
Advanced Considerations & Applications
The event model scales to Claude's more advanced capabilities without changing its shape, which is what makes it composable.
A tool call streams its input as a JSON string built up incrementally across many input_json_delta events - the partial string is not valid JSON until the block closes, so a client must buffer every delta for that block and only attempt to parse once content_block_stop fires for it. See Handling Partial JSON During Streamed Tool Calls for the accumulation pattern.
Extended thinking, when enabled, streams its reasoning as a separate content block using thinking_delta events, ahead of the text block - a UI can render that block distinctly (e.g., a collapsed "reasoning" panel) rather than mixing it into the answer text. See Streaming Extended Thinking Blocks to the Frontend.
Because tool calls stream through the same event types as text, a client can watch one stream for both: render text as it arrives, and the moment a content_block_start reports type: "tool_use", know it is time to accumulate arguments instead of displaying them - this is the basis of combining streaming with the tool-use loop.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Non-streaming request | Simplest client code; one parse of one JSON object | User waits for the full response before seeing anything | Background jobs, batch generation, server-to-server calls with no live viewer |
| SSE streaming | Renders output as it's generated; lower perceived latency | Requires event-type handling, partial-state buffering, reconnect logic | Chat UIs, coding assistants, any surface a user is watching in real time |
| Streaming + tool use | Same responsiveness, plus mid-response tool calls | Must distinguish text blocks from tool_use blocks, buffer JSON deltas | Agentic loops where the user should see reasoning/text before a tool result returns |
At the transport level, SSE rides over the same HTTP connection as any other API call, so it inherits ordinary network failure modes - proxies that buffer output, load balancers with idle timeouts, and connections that simply drop mid-response.
Production code has to treat the stream as unreliable: buffer what has arrived, detect a drop, and decide whether to resume or restart the request. See Streaming Best Practices for the concrete patterns.
Common Misconceptions
- "Each SSE event is one token." In practice a
text_deltacan span multiple tokens or be a sub-token fragment - never assume a fixed granularity, always concatenate. - "The stream sends the whole message and I filter it client-side." The server does the incremental generation; the client only ever sees what has been generated so far, not the full message in advance.
- "
content_block_deltaalways means text." It's a generic envelope - its innerdelta.typedetermines whether it'stext_delta,input_json_delta, orthinking_delta. - "Streaming is a different API." It's the same Messages API and the same request body, with
stream=true(or the SDK's streaming helper) changing only how the response is delivered. - "A dropped connection mid-stream still gives you a valid partial message." A drop can happen at any byte boundary; the client must handle an incomplete block gracefully, not assume it received a clean prefix.
FAQs
What does "SSE" actually stand for and is it Claude-specific?
Server-Sent Events. It's a general web standard (not an Anthropic invention) for one-way, server-to-client streaming over HTTP, also used by many other APIs and web frameworks.
Do I need to hand-parse the SSE wire format myself?
No. The official anthropic Python SDK's client.messages.stream(...) context manager parses the SSE frames for you and yields typed event objects - you work with event.type and event.delta, not raw data: lines.
What is the very first event I'll see in any stream?
message_start. It carries the message shell - id, model, role - before any content block has begun.
How do I know when the whole response is done?
The message_stop event is always last. Between message_delta (which carries stop_reason and final usage) and message_stop, the response is effectively complete.
Can a single response stream more than one content block?
Yes. A response can contain multiple blocks in sequence - for example a thinking block, then a text block, then a tool_use block - each with its own content_block_start/content_block_stop pair and index.
Is `text_delta` guaranteed to break on word boundaries?
No. Treat each delta as an opaque text fragment and concatenate them in order; never assume it starts or ends on a whitespace or word boundary.
What's the difference between `content_block_delta` and `message_delta`?
content_block_delta carries incremental content inside one block (text, tool JSON, or thinking). message_delta carries message-level information that is only known once generation is finishing, like stop_reason and cumulative token usage.
How does a streamed tool call look different from streamed text?
The block's content_block_start reports type: "tool_use" instead of type: "text", and its deltas arrive as input_json_delta (a growing JSON string) instead of text_delta.
Why can't I parse a tool call's arguments delta-by-delta?
Each input_json_delta is a fragment of a JSON string under construction - it is not valid JSON on its own. You must accumulate every fragment for that block and parse the joined string only after the block closes.
Does streaming change what the model generates?
No. The request body and the model's output are the same either way - streaming changes only the delivery mechanism, not the content generated.
What happens if the connection drops mid-stream?
You get whatever events arrived before the drop and no message_stop. Production clients need to detect this (e.g., a timeout with no new events) and decide whether to retry the request, since SSE itself does not guarantee delivery of the remaining events.
Is streaming required for tool use or extended thinking to work?
No, both work in non-streaming requests too. Streaming is purely a delivery-mechanism choice that happens to also expose tool and thinking content incrementally when enabled.
Related
- Streaming Responses Basics - the first runnable example of consuming this event stream.
- Streaming Event Types Reference - a lookup table for every event type and its fields.
- Handling Partial JSON During Streamed Tool Calls - accumulating
input_json_deltasafely. - Streaming Extended Thinking Blocks to the Frontend - handling
thinking_delta. - Streaming Responses with the Python SDK - the SDK's streaming helper in more depth.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.