Response streaming
A user sends a message to your chat app. Ten seconds of blank screen pass. Then fifteen. Then twenty. By the time the full response arrives, the user has already clicked away. The problem isn't latency to first token — Claude is generating the whole time — it's that your code is waiting for the entire response before showing anything. Streaming fixes that.
What streaming means
With streaming, the API sends you the response as a series of small events while Claude is still generating it. Each event contains a fragment of the output — usually a few tokens — that you can pipe straight to the UI. The user sees text appear word by word, ChatGPT-style, instead of watching a spinner.
The underlying model isn't any faster. What changes is when you can start displaying output: streaming drops time-to-first-character from "whenever the full response finishes" to "roughly as soon as Claude emits its first token."
Stream event types
Under the hood, a streaming response is a sequence of typed events:
MessageStart— the response is starting. Contains the message ID and initial metadata.ContentBlockStart— a new content block is beginning (text, tool use, etc.).ContentBlockDelta— a fragment of the current content block. This is the event you actually care about for text streaming — each delta carries a smalltextchunk.ContentBlockStop— the current content block is finished.MessageDelta— metadata updates (usually the finalstop_reasonandusage).MessageStop— the full response is complete.
For most chat UIs you don't need to think about most of these. The SDK provides a higher-level wrapper that handles them for you.
The raw approach: stream=True
If you want direct access to every event, pass stream=True on a regular create() call:
stream = client.messages.create(
model=model,
max_tokens=1000,
messages=messages,
stream=True,
)
for event in stream:
# inspect event.type and pull deltas out of ContentBlockDelta events
...
This is what you'd reach for if you're building tooling that needs to react to every event type — custom instrumentation, partial tool-call handling, or anything where the event structure matters.
The simplified approach: client.messages.stream()
For the common case — "show the user text as it arrives" — the SDK gives you a context manager that hides the event machinery:
with client.messages.stream(
model=model,
max_tokens=1000,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
stream.text_stream is an iterator over just the text chunks. No event types, no deltas to unpack. This is the API you'll use in production chat applications 90% of the time.
Getting the final message for storage
One thing streaming makes awkward: at the end of the stream, you've emitted the response to the UI piece by piece, but you also need to store the full assistant message in your conversation history so the next turn has context. Reassembling it yourself from the deltas is painful. The SDK does it for you:
with client.messages.stream(
model=model,
max_tokens=1000,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final_message = stream.get_final_message()
add_assistant_message(messages, final_message.content[0].text)
stream.get_final_message() returns the complete assembled message — same shape as a non-streaming response — with full content, usage, and stop_reason. Call it after the stream has drained, inside the with block, and you have both the streaming UX and a clean message to persist.
Use streaming any time a human is waiting on the other end. Background jobs and batch pipelines don't need it. Anything with a user-facing "thinking…" indicator does.
Key Takeaways
- 1Streaming sends the response as a sequence of small events while Claude is still generating, dropping time-to-first-character from the full response time to effectively the first token.
- 2The raw stream=True approach exposes typed events (MessageStart, ContentBlockDelta, MessageStop, and others) for tooling that needs fine-grained control.
- 3For standard chat UIs, client.messages.stream() with stream.text_stream gives you a simple iterator over text chunks — this is the production default.
- 4Call stream.get_final_message() after the stream drains to get the fully assembled assistant message for storage in conversation history.
- 5Stream any time a human is waiting on the other end; skip it for background and batch work where no one sees the spinner.