AI

Claude API Streaming With FastAPI SSE and Token Cost Tradeoffs

Claude API Streaming With FastAPI SSE and Token Cost Tradeoffs

Streaming responses changed how we build AI features. Not in some abstract way — in the very concrete sense that users stopped abandoning chat windows after 8 seconds of staring at a spinner.

Eighteen months ago, most Claude integrations waited for the full response before sending anything. Today, production teams running Claude API streaming workflows expect sub-200ms time-to-first-token. The architecture to get there isn’t complicated, but the cost math is often wrong — and that’s where projects quietly bleed money.

This piece breaks down the full stack: SSE implementation in FastAPI, real token counting during streaming, and the cost model that actually holds up under production load.


In brief: Streaming Claude API responses via FastAPI SSE cuts perceived latency dramatically, but introduces token accounting complexity that most tutorials skip entirely.

  1. According to the Claude API documentation (platform.claude.com), streaming returns input_tokens and output_tokens in the final message_delta event — not incrementally, meaning naive per-chunk counting is wrong.
  2. FastAPI’s StreamingResponse with text/event-stream content type handles SSE natively without additional libraries.
  3. Production cost tracking requires capturing the terminal message_stop event, not summing mid-stream deltas, to get accurate billing data.

Why Streaming Claude in FastAPI Became the Default Pattern

A year ago, the typical Claude integration looked like this: POST to Anthropic’s API, wait 10–25 seconds, return a JSON blob. Fine for batch jobs. Terrible for chat interfaces, document analysis tools, or anything a human watches in real time.

FastAPI became the framework of choice for Python-based AI backends primarily because of its async-first design. Unlike Flask, where streaming requires awkward generator hacks, FastAPI’s StreamingResponse is a first-class citizen. Pair that with Python’s httpx async client and Anthropic’s official Python SDK — version 0.25+ added native async streaming support — and the implementation becomes surprisingly clean.

The Anthropic Python SDK’s stream() context manager handles reconnection, delta assembly, and event typing. What it doesn’t handle is your business logic around cost tracking. That’s on you.

By early 2026, Claude 3.5 Sonnet and Claude 3 Opus are both priced per token: $3/million input tokens and $15/million output tokens for Opus, with matching rates for Sonnet (per Anthropic’s published pricing page, Q1 2026). At scale, miscounting output tokens by even 15% compounds fast.


Implementing SSE with FastAPI: The Actual Code Path

The core pattern is straightforward. FastAPI’s StreamingResponse accepts an async generator, and you yield SSE-formatted strings:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import anthropic

app = FastAPI()
client = anthropic.AsyncAnthropic()

async def stream_claude(prompt: str):
    async with client.messages.stream(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    ) as stream:
        async for text in stream.text_stream:
            yield f"data: {text}\n\n"
        
        # Capture final usage AFTER stream completes
        final_message = await stream.get_final_message()
        usage = final_message.usage
        yield f"data: [USAGE] input={usage.input_tokens} output={usage.output_tokens}\n\n"

@app.get("/stream")
async def stream_endpoint(prompt: str):
    return StreamingResponse(
        stream_claude(prompt),
        media_type="text/event-stream"
    )

Two things worth noting. First, text_stream yields only text deltas — not the full SSE event objects. Second, get_final_message() blocks until the stream completes, which is exactly when you want to read token counts.

Token Cost Calculation: Where Most Implementations Break

This is the part tutorials skip. During streaming, the Claude API sends content_block_delta events with text chunks. These chunks don’t carry token counts. The token data arrives in the final message_delta event with usage.output_tokens.

Trying to count tokens by splitting chunks and running a local tokenizer like tiktoken produces wrong numbers. Claude uses its own tokenization scheme. The usage object in the terminal event is authoritative — everything else is an estimate.

A production cost logger looks like this:

import time

class StreamCostTracker:
    def __init__(self, model: str):
        self.model = model
        self.start_time = time.time()
        self.input_tokens = 0
        self.output_tokens = 0
        
    def record_usage(self, usage):
        self.input_tokens = usage.input_tokens
        self.output_tokens = usage.output_tokens
    
    def calculate_cost(self) -> float:
        # Claude 3 Opus pricing (Q1 2026)
        input_cost = (self.input_tokens / 1_000_000) * 3.00
        output_cost = (self.output_tokens / 1_000_000) * 15.00
        return input_cost + output_cost

One real-world number worth anchoring this to: a document summarization app processing 500 daily requests at roughly 800 input tokens and 400 output tokens per request runs about $4.50/day on Opus. That’s $135/month. Miscounting output tokens by 20% means you’re budgeting $108 when actual costs hit $135. At 50,000 requests/day, that gap becomes $2,700/month in unplanned spend — the kind of number that shows up as a line-item surprise in quarterly reviews.

Handling Streaming Failures and Reconnection

Production SSE connections drop. Mobile clients go to sleep. CDNs time out idle connections after 30–60 seconds.

FastAPI doesn’t auto-reconnect SSE streams — that’s the browser’s job via the EventSource API’s built-in retry logic. Server-side, you need to handle httpx.RemoteProtocolError and anthropic.APIConnectionError gracefully:

async def stream_with_retry(prompt: str, max_retries: int = 2):
    for attempt in range(max_retries):
        try:
            async with client.messages.stream(...) as stream:
                async for text in stream.text_stream:
                    yield f"data: {text}\n\n"
                return
        except anthropic.APIConnectionError:
            if attempt == max_retries - 1:
                yield "data: [ERROR] Connection failed\n\n"

This approach can fail when a partial response has already been sent to the client before the connection drops. There’s no clean way to resume mid-stream — the client receives an error event and must decide whether to retry from scratch or display the partial output. Build your frontend to handle both cases explicitly.

Streaming Architecture Comparison

ApproachLatency (TTFT)Cost Tracking AccuracyComplexityBest For
Sync (no streaming)8–25s✅ Exact from responseLowBatch jobs, webhooks
SSE via FastAPI~150ms✅ From final eventMediumChat, live UIs
WebSocket streaming~120ms✅ From final eventHighBidirectional apps
Polling chunks~500ms❌ Often miscountedMediumLegacy systems

FastAPI SSE hits the sweet spot. WebSockets add bidirectional overhead you don’t need for one-way AI responses. Polling introduces latency and cost tracking errors. Sync responses destroy UX for anything over 3 seconds.

That said, SSE isn’t always the right answer. If your use case involves bidirectional communication — a voice interface, a collaborative editing session, or anything where the client sends data while the server is still responding — WebSockets are the better fit despite the added complexity.


Practical Implications: Three Scenarios Worth Planning For

Scenario 1 — Multi-tenant SaaS with per-user billing. Each user’s token usage needs attribution. Pass a user_id into the cost tracker and write to a database after the stream’s message_stop event fires. Don’t estimate mid-stream.

Recommendation: Store (user_id, request_id, input_tokens, output_tokens, model, timestamp) in a write-optimized table. Run cost aggregation as a nightly job, not per-request.

Scenario 2 — High-concurrency document processing (100+ simultaneous streams). FastAPI’s async handling means you’re not blocked per connection, but each active stream holds an open HTTP/2 connection to Anthropic. Watch your connection pool settings in httpx.AsyncClient — the default limits.max_connections=100 will bottleneck at scale.

Recommendation: Set max_connections=500 and max_keepalive_connections=200 for high-throughput deployments. Monitor with Prometheus httpx_connections_active metric.

Scenario 3 — Streaming behind a load balancer. AWS ALB and most nginx configurations strip Transfer-Encoding: chunked headers or buffer responses. SSE breaks silently — and that last word is the important one. No errors, no warnings, just users staring at blank screens.

Recommendation: Nginx needs proxy_buffering off; and X-Accel-Buffering: no headers. AWS ALB requires enabling HTTP/2 and setting idle timeout above your expected response length. Test this in staging before it bites you in production.

What to watch in Q3 2026: Anthropic’s prompt caching feature, currently in beta, promises 90% cost reduction on repeated context. If it reaches general availability, the input token cost math changes entirely for apps that prepend large system prompts. Worth building your cost tracker to accommodate a variable input rate now, rather than refactoring later.


Conclusion

Three things the data makes clear about production Claude streaming work:

  • Token counts live in the final event. Any mid-stream counting approach gives wrong numbers.
  • FastAPI + SSE is the lowest-complexity path to production streaming — not WebSockets, not polling.
  • Infrastructure surprises eat streaming projects — load balancer buffering and connection pool limits cause more production incidents than application code does.

Over the next 6–12 months, expect Anthropic’s prompt caching to become standard practice, cutting input costs by 50–90% for apps with large static system prompts. Streaming latency will likely improve as Anthropic expands regional inference endpoints beyond US-East.

The open question worth tracking: whether Anthropic releases token-level streaming metadata before end of 2026. Right now, accurate billing requires waiting for the stream to finish. Token-level cost events would unlock real-time budget enforcement — genuinely useful for consumer apps with hard spending caps.

Build the cost tracker first. Everything else is recoverable.


Key Takeaways

  • The usage object in the terminal message_stop event is the only authoritative token count — mid-stream estimates will skew your billing
  • FastAPI’s StreamingResponse with text/event-stream is production-ready out of the box; no extra libraries needed
  • Nginx and AWS ALB buffer SSE by default — configure proxy_buffering off before deploying, not after
  • Connection pool limits in httpx.AsyncClient are the most common bottleneck at 100+ concurrent streams
  • Prompt caching (currently in beta) could cut input token costs by up to 90% — design your cost model to handle variable input rates now

Sources: Claude API Streaming Documentation (platform.claude.com/docs/en/build-with-claude/streaming); Anthropic API Pricing Page (anthropic.com/pricing, Q1 2026); Anthropic Python SDK changelog (github.com/anthropics/anthropic-sdk-python).

References

  1. Streaming Messages - Claude API Docs
  2. Streaming Claude agent responses in production: real-time output without waiting for completion - Un
  3. Claude API Streaming: Real-Time AI Responses in Production | CallSphere Blog

Photo by Bernd 📷 Dittrich on Unsplash