Streaming Best Practices
Streaming is unreliable by nature - long-lived connections drop, proxies buffer output, and clients disconnect mid-response.
This page collects the practices that keep a streaming integration solid once it leaves a demo and reaches production traffic.
How to Use This List
- Treat the lettered groups as layers: transport reliability first, then correctness of accumulated state, then UX and cost.
- Every rule ties back to a concrete failure mode - if you can't picture the failure, the rule probably doesn't apply to your surface yet.
- Revisit this list whenever you add a new consumer of a stream (a new UI, a new relay endpoint) since each one re-introduces the same failure modes independently.
A - Connection Reliability
- Wrap every stream in a
try/exceptthat covers the entire iteration, not just the call that opens it.anthropic.APIStatusErrorand its subclasses (likeRateLimitError,APIConnectionError) can surface partway through consuming the stream, not only when it's first opened. - Set an explicit request timeout distinct from your retry policy. A stream that goes silent (no new events) for longer than a reasonable window should be treated as failed and retried, not waited on indefinitely.
- Retry with exponential backoff on transient errors, not on every error. Retry
APIConnectionErrorand 5xx-classAPIStatusErrors; do not retry 4xx errors like a malformed request, which will fail identically every time. - Never retry a partially-streamed response by resuming mid-stream. The Messages API streaming endpoint doesn't support resuming a dropped connection from where it left off - a retry means re-sending the full request and starting a new stream.
- Cap total retry attempts and surface a clear failure to the user after the cap. An unbounded retry loop against a persistently failing backend just burns time and API calls.
B - Buffering & State
- Buffer every content block's deltas independently, keyed by
index. A response can contain multiple blocks (text, tool_use, thinking) interleaved by index; a single global buffer corrupts state the moment more than one block streams. - Never parse
input_json_deltafragments individually. Accumulate the full string for a block and parse once, atcontent_block_stop- see Handling Partial JSON During Streamed Tool Calls. - Treat
text_deltaas an opaque fragment, never a token or word boundary. Sentence-detection, profanity filtering, or markdown parsing logic should run on the accumulated string, not on individual deltas. - Batch UI updates instead of re-rendering on every delta. Flushing accumulated text to the UI every 25-50ms (rather than per-delta) cuts render overhead substantially with no perceptible latency cost.
- Call
get_final_message()only after the stream has been fully iterated. Calling it early can return an incompleteMessageobject.
C - Error Handling
- Distinguish a clean
message_stopfrom a stream that just went silent. If your loop exits without ever seeingmessage_stop, treat that as a failure condition and log it distinctly from a successful completion. - Handle the
errorevent type explicitly, not just exceptions. A mid-streamerrorevent (e.g. from an overloaded model) is a normal part of the event vocabulary, not always raised as a Python exception depending on SDK version - check both paths. - Fail loudly on unexpected
stop_reasonvalues in code paths that assume completion. If your code expects"end_turn"but gets"max_tokens", the response was truncated - don't silently treat it as complete. - Log the accumulated partial content on failure, not just the error message. A partial response is often useful for debugging (and sometimes salvageable for the user) even when the stream ultimately failed.
- Validate tool call JSON after parsing, not just that it parsed. Successfully parsed JSON that's missing a required field from the tool's
input_schemais still a malformed call your execution code needs to reject cleanly.
D - Reconnect Strategy
- Re-send the full request on reconnect rather than attempting partial resume. Since the API has no resume capability, your "reconnect" is really "retry the whole request" - design your retry loop around that assumption from the start.
- Decide up front whether a reconnect restarts the user-visible message or appends to it. Silently restarting a half-shown chat bubble looks buggy; clear it and show a retry indicator instead.
- Use idempotency-safe application logic around tool execution before a retry. If a tool call streamed completely and executed before the connection dropped on a later block, a naive retry of the whole request could re-execute that tool a second time - guard side-effecting tools accordingly.
- Bound reconnect attempts with the same backoff and cap as connection retries (Group A). A reconnect loop is a retry loop; don't maintain separate, inconsistent policies for the two.
E - Cost & Observability
- Close the stream explicitly when a client disconnects. An abandoned browser tab that isn't detected server-side keeps consuming (and billing) output tokens for a response nobody will see.
- Log
usagefrommessage_delta, not an estimate. Token counts are only exact once the API reports them - counting characters or words client-side is not a reliable proxy for billing or rate-limit accounting. - Track
stop_reasondistribution in production metrics. A rising rate of"max_tokens"stops usually means yourmax_tokenscap is too low for real traffic, not that responses are naturally getting longer. - Alert on a rising rate of
errorevents or failed reconnects, not just on outright request failures. Streaming failures can hide inside a "successful" HTTP 200 that never reachesmessage_stop.
FAQs
What's the single highest-priority practice on this list?
Wrapping the entire streaming iteration - not just the initial call - in error handling. Most production streaming bugs come from an error surfacing mid-stream that the code never expected to catch there.
Can I resume a dropped stream from where it left off?
No. The Messages API streaming endpoint has no resume capability - a "reconnect" is really re-sending the full request and starting a fresh stream.
How do I tell a successful stream from one that silently failed?
A successful stream's loop exits after observing message_stop. If your loop exits (or the connection closes) without ever seeing message_stop, treat it as a failure, log it, and consider a retry.
Should I retry every error the same way?
No. Retry transient errors like APIConnectionError or 5xx APIStatusErrors with backoff. Don't retry 4xx errors (like an invalid request) - they'll fail identically every time.
Is it safe to re-render the full accumulated text on every single delta?
It works, but at scale it's wasteful. Batching UI updates over a short interval (25-50ms) instead of per-delta reduces render cost with no perceptible impact on user experience.
What happens if a user closes their browser tab mid-stream?
Nothing stops automatically on the Claude side unless your backend detects the client disconnect and closes its own stream - otherwise you keep paying for output tokens nobody will see.
How should I handle a tool call that partially executed before a connection drop?
Be cautious about idempotency: if the tool already executed and had a side effect before the drop, blindly retrying the whole request could re-trigger that side effect. Guard side-effecting tools with idempotency keys or dedup logic.
What does a rising rate of `max_tokens` stop reasons usually indicate?
Your max_tokens cap is too low for the responses your traffic is actually generating - it's a signal to raise the cap or investigate why responses are longer than expected, not something to silently ignore.
Do I need different retry logic for streaming versus non-streaming requests?
The retry decision logic (which errors to retry, backoff strategy, caps) is the same. The difference is only that a streaming failure can occur after the request already appeared to succeed (mid-stream), so detection requires watching the whole iteration, not just the initial response.
Should I log the full accumulated text when a stream fails?
Yes - the partial content up to the failure point is valuable for debugging and, in some UIs, can be shown to the user with a "response was interrupted" notice rather than discarded entirely.
Related
- How Server-Sent Events Power Claude's Streaming Responses - the event model these practices operate on.
- Handling Partial JSON During Streamed Tool Calls - the buffering pattern referenced in Group B.
- Combining Streaming with the Tool Use Loop - idempotency considerations for tool execution referenced in Group D.
- Python SDK Retry and Timeout Configuration Reference - the SDK-level settings behind Group A.
- Python SDK Exception Types at a Glance - which exceptions are safe to retry.
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.