CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
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.

streaming21troubleshooting87api38rate-limit7timeout9python22

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-04-07
How to Fix Claude API 429, 503 Errors and Timeouts: A Complete Troubleshooting Guide
Struggling with Claude API 429 rate limit errors, 503 service unavailable responses, or timeout failures? This guide covers root causes and step-by-step fixes including exponential backoff, concurrency control, and Tier upgrades.
API & SDK2026-06-27
When Claude API Streaming Stops Without an Error: Detecting Silent Stalls and Resuming Mid-Stream
How to catch the 'silent stall' where Claude API streaming stops with no exception at all, using a content-level watchdog that times the gap between tokens, plus a resume path that carries received text forward as an assistant prefill, and a four-layer timeout budget for long-running automation.
API & SDK2026-05-16
Debugging Claude API Tool Use Schema Errors: 3 Patterns I've Hit and How to Fix Them
A practical guide to diagnosing Claude API Tool Use errors—from schema definition mistakes to invalid_tool_use blocks and Claude ignoring your tools entirely. Based on real implementation experience.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →