◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-04-26Intermediate

Claude API Streaming Stops Mid-Response: Diagnosing and Fixing the 5 Root Causes

When Claude API streaming stops unexpectedly, there are exactly 5 root causes. Learn to diagnose which one you're hitting and apply the right fix — from timeout tuning to stop_reason logging.

streaming22troubleshooting92api39rate-limit7timeout10python22

Your Claude API integration is streaming responses beautifully — until it isn't. The stream stops mid-sentence, sometimes with an error, sometimes in complete silence. You add a retry and it works once, then fails again. Sound familiar?

The maddening part is that "streaming stopped" can mean five completely different things, each requiring a different fix. Throwing retries at a max_tokens issue, or increasing timeouts for a rate-limit problem, won't help. Let's cut through this systematically.

Step One: Classify the Failure by Symptom

Before touching any code, identify which category you're in.

If an exception was raised:

429 RateLimitError          → Rate limiting (Cause 2)
529 OverloadedError         → Server overload (Cause 2, same fix)
400 Bad Request             → Context window exceeded (Cause 3)
ReadTimeout / TimeoutError  → Network timeout (Cause 1)
ECONNRESET                  → Connection reset (Cause 1)

If the stream ended silently (no exception):

stop_reason: "max_tokens"   → max_tokens setting too low (Cause 4)
stop_reason: "end_turn"     → Normal completion — working as expected
Events just stop arriving   → Client-side implementation bug (Cause 5)

The fastest diagnostic step you can take right now: add stop_reason logging after every stream. I've seen developers spend hours on a "streaming bug" that was simply max_tokens: 256 on a request asking for a 2,000-word document.

Cause 1: Network Timeout (Most Common)

Long responses get cut by HTTP timeout — your client gives up waiting while Claude is still generating. Default timeouts in requests and httpx are typically 30–60 seconds, which is not enough for complex Claude responses.

import anthropic
import httpx
 
# Default: will timeout on long responses
client_default = anthropic.Anthropic()
 
# Production: explicit timeout tuning for streaming
client = anthropic.Anthropic(
    timeout=httpx.Timeout(
        connect=10.0,   # Time to establish connection
        read=300.0,     # Time to receive data — this is what matters for streaming
        write=10.0,     # Time to send request
        pool=10.0,      # Connection pool timeout
    )
)
 
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=4096,
    messages=[{"role": "user", "content": "Write a detailed technical report on..."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

read=300.0 (5 minutes) may seem generous, but complex Claude tasks legitimately take 2–3 minutes. Since adopting this in production, I've had zero timeout-related stream cuts.

Cause 2: Rate Limits and Server Overload (429 / 529)

429 RateLimitError means you've exceeded your requests-per-minute or tokens-per-minute quota. 529 OverloadedError is temporary server-side capacity pressure. Both recover with time — the correct response is exponential backoff with retry.

import anthropic
import time
 
def stream_with_retry(client, max_retries=3, **kwargs):
    """Streaming with exponential backoff for rate limits and overload."""
    for attempt in range(max_retries):
        try:
            with client.messages.stream(**kwargs) as stream:
                collected = ""
                for text in stream.text_stream:
                    print(text, end="", flush=True)
                    collected += text
                return collected
        except anthropic.RateLimitError:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) * 30  # 30s → 60s → 120s
            print(f"\n⚠️ Rate limited — waiting {wait}s ({attempt+1}/{max_retries})")
            time.sleep(wait)
        except anthropic.APIStatusError as e:
            if e.status_code == 529 and attempt < max_retries - 1:
                wait = (2 ** attempt) * 10
                print(f"\n⚠️ Overloaded — waiting {wait}s ({attempt+1}/{max_retries})")
                time.sleep(wait)
            else:
                raise
 
client = anthropic.Anthropic()
stream_with_retry(
    client,
    model="claude-sonnet-4-6",
    max_tokens=2048,
    messages=[{"role": "user", "content": "Hello!"}],
)

If you're hitting rate limits frequently, check your tier in the Anthropic console. Tier 1 has lower TPM limits that can catch you off guard with concurrent requests. For a full breakdown of 429 handling, see Claude API 429 Error Handling Guide.

Cause 3: Context Window Exceeded (400 Error)

When total input tokens exceed the model's context window, you get a 400 Bad Request before streaming even starts — so the symptom is "stream never begins" rather than "stream stops mid-way."

The same issue occurs when input_tokens + max_tokens would exceed the limit.

import anthropic
 
client = anthropic.Anthropic()
 
def get_safe_max_tokens(messages, model="claude-sonnet-4-6", buffer=100):
    """Calculate safe max_tokens based on actual input length."""
    count = client.messages.count_tokens(
        model=model,
        messages=messages,
    )
    input_tokens = count.input_tokens
    
    # claude-sonnet-4-6: 200k context window, 64k max output
    context_window = 200000
    max_output = 64000
    
    available = context_window - input_tokens - buffer
    safe_tokens = min(available, max_output)
    
    print(f"Input: {input_tokens} tokens / Safe max_tokens: {safe_tokens}")
    return max(safe_tokens, 1)
 
messages = [
    {"role": "user", "content": "Here is a very long document..."}
]
 
safe_tokens = get_safe_max_tokens(messages)
 
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=safe_tokens,
    messages=messages,
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

If you're building a multi-turn chatbot and passing full conversation history, this becomes an issue around message 10–15. Consider summarizing older turns or using a sliding window of recent messages.

Cause 4: max_tokens Reached (Normal End, Wrong Setting)

This is not an error — it's expected behavior — but it looks like a bug because the response is cut short. The tell: stop_reason will be "max_tokens" instead of "end_turn".

import anthropic
 
client = anthropic.Anthropic()
 
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=256,  # Too low for "explain in detail" requests
    messages=[{"role": "user", "content": "Explain Python async/await in detail"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    
    # Always check stop_reason after stream ends
    final = stream.get_final_message()
    print(f"\n\n[stop_reason: {final.stop_reason}]")
    
    if final.stop_reason == "max_tokens":
        print("⚠️ Response was cut short. Increase max_tokens.")
    elif final.stop_reason == "end_turn":
        print("✅ Response completed normally.")

As a rule of thumb: for conversational responses, max_tokens=1024 is usually fine. For detailed technical content, use 4096 or higher. claude-sonnet-4-6 supports up to 64,000 output tokens.

Cause 5: Client-Side Implementation Bugs

When there's no error and stop_reason is end_turn, but the output still seems truncated, look at your client code.

Using sync client in async code:

import asyncio
import anthropic
 
# Wrong: using sync client in async context
async def wrong():
    client = anthropic.Anthropic()  # sync client
    # This will block the event loop
 
# Correct: use AsyncAnthropic for async code
async def correct():
    client = anthropic.AsyncAnthropic()
    
    async with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello"}],
    ) as stream:
        async for text in stream.text_stream:
            print(text, end="", flush=True)
    print()
 
asyncio.run(correct())

Breaking out of the stream loop early:

# This cuts the stream — remaining events are lost
with client.messages.stream(...) as stream:
    for text in stream.text_stream:
        if some_condition:
            break  # Stream is closed here
 
# If you need the full text without streaming display, use get_final_message
message = client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello"}],
).get_final_message()
print(message.content[0].text)

Production-Ready Streaming Implementation

Here's a battle-tested pattern combining all five fixes:

import anthropic
import httpx
import time
from collections.abc import Iterator
 
def create_client() -> anthropic.Anthropic:
    """Initialize client with production-appropriate timeouts."""
    return anthropic.Anthropic(
        timeout=httpx.Timeout(connect=10.0, read=300.0, write=10.0, pool=10.0)
    )
 
def stream_robust(
    client: anthropic.Anthropic,
    messages: list,
    model: str = "claude-sonnet-4-6",
    max_tokens: int = 4096,
    max_retries: int = 3,
) -> Iterator[str]:
    """
    Production streaming with automatic diagnosis and retry.
    Handles: timeout, rate limit, overload.
    Logs stop_reason for visibility.
    """
    for attempt in range(max_retries):
        try:
            with client.messages.stream(
                model=model,
                max_tokens=max_tokens,
                messages=messages,
            ) as stream:
                for text in stream.text_stream:
                    yield text
                
                final = stream.get_final_message()
                if final.stop_reason == "max_tokens":
                    print(f"\n⚠️ Hit max_tokens ({max_tokens}). Consider increasing it.")
                return  # Normal completion
        
        except anthropic.RateLimitError:
            wait = 2 ** attempt * 30
            if attempt < max_retries - 1:
                print(f"\n⚠️ Rate limited — {wait}s backoff ({attempt+1}/{max_retries})")
                time.sleep(wait)
            else:
                raise
        
        except anthropic.APIStatusError as e:
            if e.status_code == 529 and attempt < max_retries - 1:
                wait = 2 ** attempt * 10
                print(f"\n⚠️ Overloaded — {wait}s backoff ({attempt+1}/{max_retries})")
                time.sleep(wait)
            else:
                raise
        
        except (httpx.ReadTimeout, httpx.ConnectError) as e:
            if attempt < max_retries - 1:
                print(f"\n⚠️ Network error: {e} — retrying ({attempt+1}/{max_retries})")
                time.sleep(5)
            else:
                raise
 
# Usage
client = create_client()
for text in stream_robust(
    client,
    messages=[{"role": "user", "content": "Explain async/await with examples"}],
):
    print(text, end="", flush=True)

For the full error handling reference, see Claude API Error Complete Guide.

TypeScript / Node.js Equivalent

If you're using the TypeScript SDK, the pattern is nearly identical:

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic({
  timeout: 300 * 1000, // 300 seconds in milliseconds
});
 
async function streamWithRetry(
  messages: Anthropic.Messages.MessageParam[],
  maxRetries = 3
): Promise<string> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      let collected = "";
 
      const stream = await client.messages.stream({
        model: "claude-sonnet-4-6",
        max_tokens: 4096,
        messages,
      });
 
      for await (const event of stream) {
        if (
          event.type === "content_block_delta" &&
          event.delta.type === "text_delta"
        ) {
          process.stdout.write(event.delta.text);
          collected += event.delta.text;
        }
      }
 
      const finalMessage = await stream.finalMessage();
      if (finalMessage.stop_reason === "max_tokens") {
        console.warn("\n⚠️ Hit max_tokens. Consider increasing the limit.");
      }
 
      return collected;
    } catch (error) {
      if (error instanceof Anthropic.RateLimitError) {
        const wait = Math.pow(2, attempt) * 30000;
        if (attempt < maxRetries - 1) {
          console.warn(`\n⚠️ Rate limited — waiting ${wait / 1000}s`);
          await new Promise((r) => setTimeout(r, wait));
        } else {
          throw error;
        }
      } else if (
        error instanceof Anthropic.APIStatusError &&
        error.status === 529
      ) {
        const wait = Math.pow(2, attempt) * 10000;
        if (attempt < maxRetries - 1) {
          console.warn(`\n⚠️ Overloaded — waiting ${wait / 1000}s`);
          await new Promise((r) => setTimeout(r, wait));
        } else {
          throw error;
        }
      } else {
        throw error;
      }
    }
  }
  throw new Error("Max retries exceeded");
}
 
// Usage
await streamWithRetry([{ role: "user", content: "Explain async/await" }]);

One key difference: the Node.js SDK uses milliseconds for the timeout option, not seconds. It's easy to accidentally set timeout: 300 thinking you're setting 300 seconds, when you're actually setting 300 milliseconds (0.3 seconds). Always multiply by 1000.

A Note on Streaming vs. Polling

For very long-running tasks, you might also consider the Messages Batches API instead of streaming. Batches are fire-and-forget — you submit a request and poll for results later. This sidesteps all five streaming failure modes at the cost of real-time display. It's worth considering when:

  • Responses routinely exceed 30,000 tokens
  • You're processing many documents in parallel
  • Real-time display is not required by the user experience

For interactive chat and live generation, streaming is still the right choice — just with the reliability patterns shown above.


When streaming stops unexpectedly, run through this checklist in order: check the error code → check stop_reason → review your client code. In my experience, adding stop_reason logging catches the majority of "mysterious" stream cuts immediately. Start there — it's a one-line addition that pays for itself the first time you debug a production incident.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ API & SDK2026-08-15
The Connection That Dies Mid-Thought Is Being Killed by Your Relay, Not the Upstream
Put a proxy or Worker in front of Claude Code and long thinking pauses start dying. The cause is neither the model nor the upstream — it is your relay holding the byte stream. Six relay behaviors compared side by side, plus how to verify yours before it ships.
⬡ API & SDK2026-05-16
Automating Multilingual App Review Replies with Claude API — Living with the App Store's Undocumented Limits
An indie developer running a portfolio of wallpaper and healing apps shares the full implementation of Claude API-powered multilingual review reply automation — including App Store's undocumented 8-second rule, session limits, and the three traps that can get you banned.
⬡ API & SDK2026-04-26
Building a Scalable Real-Time AI Chat Server with Claude API × WebSocket × Redis Pub/Sub — Node.js Production Architecture, Multi-User Management, and Cost Control
Running a real-time AI chat server on Claude API, WebSocket, and Redis Pub/Sub in production. SSE trade-offs, multi-instance routing, and how stop latency quietly corrupts per-user budgets — with the instrumentation code to measure it.