Claude API Streaming Messages Tutorial 2026: Real-Time Responses in Python and C#
AI & Productivity

Claude API Streaming Messages Tutorial 2026: Real-Time Responses in Python and C#

Ricardo Gil
November 4, 2026
11 min read
#claude-api #streaming #python #dotnet #developer-productivity

When you call the Claude API without streaming, the response doesn't come back until the model finishes generating the entire message. For short completions that's fine. But for anything over a few hundred tokens β€” a detailed code review, a long explanation, a multi-step analysis β€” your users sit staring at a blank screen for several seconds, then get a wall of text all at once. That's a bad experience, and it's completely avoidable.

Streaming solves this by sending the response incrementally, token by token, as the model generates it. The user sees text appearing immediately, which makes a 10-second response feel interactive instead of frozen. This guide walks through exactly how to implement Claude API streaming in Python and C# (.NET), including raw SSE event handling, a practical web API endpoint, and how to handle errors properly. All examples use the official Anthropic SDKs and reference Anthropic's streaming documentation.

Why Streaming Changes the User Experience

The perceived performance improvement from streaming is significant. According to UX research, users tolerate blank waits of around one second before they start to feel friction. A Claude response that takes 8 seconds to complete feels instant when you see the first token arrive in 200ms β€” even though the total time is identical.

Beyond perception, streaming unlocks a few specific use cases that simply don't work with blocking responses. If you're building a code-review tool that processes a 2,000-line diff, you want the developer to start reading the first findings while the model is still analyzing the rest. If you're building a chatbot with long context, you want users to see the opening of the response rather than waiting for max_tokens to fill. Streaming is also how most chat frontends work β€” if you've ever used Claude.ai or Cursor, the typing effect you see is SSE under the hood.

For server-side batch jobs β€” bulk classification, embeddings, async processing of thousands of documents β€” streaming doesn't add value and adds complexity. That's what Message Batches are for. Use streaming for interactive, real-time user-facing features.

How Claude API Streaming Works: SSE Under the Hood

When you set "stream": true in a Messages API request, the API returns an HTTP response with Content-Type: text/event-stream and sends a sequence of server-sent events (SSE). Each event has a name and a JSON payload. Per Anthropic's API documentation, a complete streaming response follows this event sequence:

Event typeWhen it firesKey payload
message_startStart of responseMessage object with empty content, input token count
content_block_startOpening of each content blockBlock type (text, tool_use) and index
content_block_deltaEach incremental chunktext_delta for text, input_json_delta for tools
content_block_stopEnd of each content blockBlock index
message_deltaEnd of responseStop reason, cumulative output token count
message_stopFinal eventSignals stream is complete
pingDispersed throughoutKeepalive β€” ignore safely
errorOn server-side errorsError type and message (e.g. overloaded_error)

Each content_block_delta for a text block looks like this in the raw SSE wire format:

bash
event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}}

The SDKs abstract all of this, but understanding the event sequence matters when you need to handle tool use, track token counts in real time, or debug unexpected behavior. The token counts in message_delta are cumulative β€” the final output_tokens in that event is what you bill against, per Anthropic's docs.

Python: Streaming with the Anthropic SDK

Install the Anthropic Python SDK with pip install anthropic. The simplest streaming pattern uses the context manager and text_stream property, which yields only the text deltas and handles everything else internally:

python
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from env

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Review this Python function for edge cases:\n\ndef divide(a, b):\n    return a / b"}]
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
print()  # newline at end

The stream.text_stream iterator automatically skips message_start, content_block_start, ping, and all the bookkeeping events, delivering only the text chunks. It's the right choice when you just want to print or pipe the response.

When you need the complete Message object at the end β€” to log usage, check the stop reason, or extract multiple content blocks from a tool-use response β€” use get_final_message():

python
import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=2048,
    system="You are a code reviewer. Be specific and concise.",
    messages=[{"role": "user", "content": "Review the attached diff for security issues:\n\n{diff}"}]
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

    final = stream.get_final_message()
    print(f"\n\n--- Usage: {final.usage.input_tokens} in / {final.usage.output_tokens} out ---")

For fine-grained control β€” handling tool calls, capturing thinking blocks from extended thinking, or building custom event routers β€” iterate over the raw stream instead:

python
import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "What is 7 * 8?"}]
) as stream:
    for event in stream:
        match event.type:
            case "content_block_delta":
                if event.delta.type == "text_delta":
                    print(event.delta.text, end="", flush=True)
            case "message_delta":
                print(f"\nStop reason: {event.delta.stop_reason}")
                print(f"Output tokens: {event.usage.output_tokens}")
            case "error":
                print(f"Stream error: {event.error}")
                break

Async Python works identically but with AsyncAnthropic and async for. This is the pattern you'll use inside a FastAPI endpoint.

If you're building serious LLM applications, the book AI Engineering by Chip Huyen (~$55) has a thorough chapter on building streaming inference pipelines and production LLM serving patterns β€” worth reading before you scale.

C# and .NET: Streaming with the Official Anthropic SDK

Anthropic ships an official C# SDK. As of version 10+, the package on NuGet is Anthropic (the earlier tryAGI community package has moved to tryAGI.Anthropic). Install it with:

bash
dotnet add package Anthropic

The SDK requires .NET Standard 2.0 or later, so it works with .NET 6, 8, or 9. For non-streaming, you call client.Messages.Create(). For streaming, every streaming method has a Streaming suffix β€” client.Messages.CreateStreaming() β€” and returns IAsyncEnumerable:

csharp
using Anthropic;
using Anthropic.Models.Messages;

AnthropicClient client = new(); // reads ANTHROPIC_API_KEY from env

MessageCreateParams parameters = new()
{
    MaxTokens = 1024,
    Messages = [new UserMessageParam { Content = "What are the main differences between async/await in C# and Python?" }],
    Model = Model.ClaudeOpus5_5,
};

await foreach (var @event in client.Messages.CreateStreaming(parameters))
{
    Console.Write(@event); // prints each SSE event as it arrives
}
Console.WriteLine();

For text extraction, filter to content_block_delta events and check the delta type. Per the official SDK docs, error handling in streaming uses specific exception types. Rate limit and overload errors during streaming surface as AnthropicSseException rather than the standard AnthropicRateLimitException (which applies to non-streaming HTTP errors):

csharp
using Anthropic;
using Anthropic.Models.Messages;

AnthropicClient client = new() { MaxRetries = 2 };

MessageCreateParams parameters = new()
{
    MaxTokens = 2048,
    System = "You are a helpful .NET developer assistant.",
    Messages = [new UserMessageParam { Content = "Explain the difference between Task and ValueTask in C#" }],
    Model = Model.ClaudeOpus5_5,
};

try
{
    await foreach (var @event in client.Messages.CreateStreaming(parameters))
    {
        // Print each SSE event chunk as it arrives
        Console.Write(@event);
        await Console.Out.FlushAsync();
    }
}
catch (AnthropicRateLimitException ex)
{
    Console.Error.WriteLine($"Rate limited: retry after {ex.RetryAfter?.TotalSeconds}s");
    throw;
}
catch (AnthropicSseException ex)
{
    Console.Error.WriteLine($"SSE stream error: {ex.Message}");
    throw;
}
catch (AnthropicIOException ex)
{
    Console.Error.WriteLine($"Network error during stream: {ex.Message}");
    throw;
}

The SDK automatically retries 2 times by default on 429 and 5xx errors. For streaming, configure the client-level MaxRetries as shown β€” retries apply to the initial request, not mid-stream reconnection.

If you spend significant time writing C# and want a solid reference for LLM integration patterns, Hands-On Large Language Models (~$55) covers building real production apps end to end, with examples that map well to .NET-style architecture thinking.

Building a Streaming Chat Endpoint in ASP.NET Core

The most common use case for streaming in a .NET backend is a minimal API or controller endpoint that proxies Claude's SSE stream to a frontend. The key is setting Content-Type: text/event-stream and disabling response buffering so chunks reach the client as they arrive:

csharp
using Anthropic;
using Anthropic.Models.Messages;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<AnthropicClient>();

var app = builder.Build();

app.MapPost("/api/chat/stream", async (
    [FromBody] ChatRequest request,
    AnthropicClient anthropic,
    HttpContext ctx) =>
{
    ctx.Response.ContentType = "text/event-stream";
    ctx.Response.Headers["Cache-Control"] = "no-cache";
    ctx.Response.Headers["Connection"] = "keep-alive";

    var parameters = new MessageCreateParams
    {
        MaxTokens = 1024,
        Messages = [new UserMessageParam { Content = request.Message }],
        Model = Model.ClaudeOpus5_5,
    };

    await foreach (var @event in anthropic.Messages.CreateStreaming(parameters))
    {
        var data = $"data: {System.Text.Json.JsonSerializer.Serialize(@event)}\n\n";
        await ctx.Response.WriteAsync(data);
        await ctx.Response.Body.FlushAsync();
    }

    await ctx.Response.WriteAsync("data: [DONE]\n\n");
    await ctx.Response.Body.FlushAsync();
});

app.Run();

record ChatRequest(string Message);

On the Angular frontend, consume this with the native EventSource API or the Fetch API with a readable stream reader β€” both work. EventSource gives you automatic reconnect logic; the Fetch stream approach gives you more control over headers (useful for auth). For an Angular 17+ signal-based architecture, the Fetch stream approach integrates more cleanly with signals and toSignal().

One gotcha specific to .NET: response buffering middleware (like UseResponseBuffering from some older packages) will eat your SSE events. Make sure you're not layering any buffering middleware between the endpoint and the client. Kestrel and IIS Express both handle chunked transfer encoding correctly without additional configuration.

Error Handling and Reconnection

The Claude API can send error events mid-stream, most commonly overloaded_error during peak usage. Per Anthropic's docs, this is equivalent to an HTTP 529 in a non-streaming context. Your stream handler needs to detect these and surface them appropriately rather than leaving the client hanging:

python
import anthropic
import time

client = anthropic.Anthropic()

def stream_with_retry(messages: list, max_retries: int = 2):
    for attempt in range(max_retries + 1):
        try:
            with client.messages.stream(
                model="claude-opus-5-5",
                max_tokens=1024,
                messages=messages,
            ) as stream:
                for event in stream:
                    if event.type == "error":
                        error_type = event.error.get("type", "unknown")
                        if error_type == "overloaded_error" and attempt < max_retries:
                            print(f"\nOverloaded, retrying in 5s (attempt {attempt + 1})...")
                            time.sleep(5 * (attempt + 1))
                            break  # exits the for loop, retries the outer while
                        else:
                            raise RuntimeError(f"Unrecoverable stream error: {error_type}")
                    elif event.type == "content_block_delta" and hasattr(event.delta, "text"):
                        yield event.delta.text
                else:
                    return  # stream completed cleanly
        except anthropic.RateLimitError:
            if attempt < max_retries:
                time.sleep(10 * (attempt + 1))
            else:
                raise

Prompt caching and streaming work together β€” if you have a large system prompt that qualifies for prompt caching, you can combine cache_control blocks with stream=True. The cache hit or miss is reflected in the message_start event's usage object via cache_read_input_tokens and cache_creation_input_tokens, per the API docs.

When NOT to Stream

Streaming adds complexity β€” SSE connection management, buffering concerns, partial-response error states, and client-side reassembly. Here's when to use standard blocking requests instead:

Use blocking (non-streaming) calls when you're running batch jobs server-side where the result goes into a database or queue rather than to a user. Use Message Batches (completely separate API endpoint) when you're processing hundreds of prompts at once and can wait up to 24 hours β€” you get a 50% token price discount in exchange. Use blocking calls when you need a simple synchronous function that transforms text and you don't want SSE complexity in your codebase. Use streaming for any user-facing chat or generation UI, any scenario where response time is noticeable, and any use case where the user benefits from seeing partial output.

For the hardware side of running Claude-powered tools locally during development, a machine with enough RAM to run your .NET/Python dev stack alongside other services matters. I run my development environment on the Beelink GTi13 Ultra (Proxmox host). If you're building a dedicated dev box, a Beelink SER5 Mini PC (~$299) is a solid, low-power machine for running local Claude API proxies and development tooling. Pair it with a Crucial 32GB DDR4 SODIMM (~$59) if you're running Ollama or multiple dev services alongside it.

Conclusion

Claude API streaming is straightforward once you understand the SSE event model. The Python SDK's text_stream iterator handles 90% of use cases with minimal code. For C#, the official Anthropic NuGet package exposes streaming via IAsyncEnumerable through CreateStreaming(), which plugs directly into ASP.NET Core's minimal API pattern. The main things to get right are error handling for overloaded_error mid-stream events, disabling response buffering in your web API, and not reaching for streaming when batch processing or simple synchronous calls are the better tool.

If you're new to the Claude API and want to understand the full breadth of features before building, the tool use tutorial covers function calling β€” the other key API primitive that works alongside streaming for agentic workflows.

One final note on model selection for streaming: Anthropic's docs currently show claude-opus-5-5 as the reference model for all streaming examples. For high-volume, latency-sensitive streaming applications, check the Anthropic models page for the latest recommended model and pricing β€” the lower-tier models in the Claude family offer faster time-to-first-token at reduced cost, which matters a lot in streaming UX.

Want Claude dialed in for your workflow?

Book a 1-on-1 Claude setup call: 60 minutes, live, in English or Spanish, recorded for you. $127. Book the call β†’

Or start with the Claude Prompt Pack: 50 bilingual prompts, $17, instant download.

Ricardo Gil is a full-stack developer (.NET/Angular) with 6+ years of experience. He uses AI tools daily β€” Claude Code, n8n automations, and local LLMs via Ollama on his homelab. More about Ricardo β†’
πŸ“¬Weekly Newsletter

Get the best home lab & AI content

No spam. One email per week. Unsubscribe anytime.

Share this article