Claude API Streaming Response in Python Flask
Streaming Claude responses from a Flask backend
If you're building a Flask app that calls the Claude API, you almost always want to stream the response rather than wait for the full completion. Streaming lets your frontend show tokens as they're generated — the same experience users get in Claude's own chat interface — instead of staring at a spinner for 5–15 seconds while a long answer finishes.
The short answer: Claude's API supports streaming via server-sent events (SSE) when you set "stream": true in your request. Flask can forward that stream to the browser using a generator function and a Response object with mimetype="text/event-stream". The tricky part isn't calling Claude — it's correctly proxying the stream through Flask without buffering it, and parsing Anthropic's SSE event types on the way through.
Setting up the Flask route
Here's a minimal working example using the anthropic Python SDK and Flask's generator-based streaming:
from flask import Flask, Response, request
import anthropic
import json
app = Flask(__name__)
client = anthropic.Anthropic(api_key="sk-ant-...")
@app.route("/chat", methods=["POST"])
def chat():
user_message = request.json.get("message", "")
def generate():
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": user_message}],
) as stream:
for text in stream.text_stream:
yield f"data: {json.dumps({'text': text})}\n\n"
yield "data: [DONE]\n\n"
return Response(generate(), mimetype="text/event-stream")
if __name__ == "__main__":
app.run(threaded=True)
A few things that matter here:
threaded=True(or running under a proper WSGI server with threading/async workers) is required, otherwise Flask's dev server will block other requests while streaming.- The generator pattern is what makes this work — Flask calls your function lazily and sends each
yieldas soon as it's produced, instead of collecting everything first. text/event-streamis the correct MIME type for SSE. If you leave it asapplication/json, browsers and most HTTP clients will buffer the whole response before delivering it.
Avoiding buffering issues
Streaming breaks most often not because of Claude's API but because something in the stack buffers the response before it reaches the client. Common culprits:
- Nginx or a reverse proxy buffering by default. Add
proxy_buffering off;for the relevant location block. - Gunicorn's sync workers. Use
gunicorn --worker-class gthreador an async worker (gevent,uvicornwith an ASGI wrapper) for production streaming. gzipcompression on the response, which forces full buffering before compressing. Disable compression for the/chatroute specifically.- Flask's
Responseobject needingdirect_passthrough=Truein some edge cases where Werkzeug tries to computeContent-Length.
If you test locally with curl -N and see the text/event-stream data arrive in bursts instead of token-by-token, one of the above is almost always the cause.
Consuming the stream from the frontend
On the browser side, use the EventSource API or a fetch call with a readable stream reader:
const response = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Explain async generators in Python" }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// parse the "data: ..." lines and append text to the UI
}
EventSource is simpler for GET requests but doesn't support custom headers or POST bodies natively, so most chat-style apps use fetch with a manual reader instead.
Handling tool use and errors in a stream
If your Claude requests involve tool calls, the stream will include content_block_start, content_block_delta, and content_block_stop events for tool-use blocks, not just text. The anthropic SDK's stream.text_stream helper only surfaces text deltas — if you need tool call data too, iterate over stream directly and check event.type. Also wrap the generator body in a try/except and yield an error event on failure, otherwise a dropped connection or rate limit mid-stream will just silently truncate the response with no signal to the frontend.
def generate():
try:
with client.messages.stream(...) as stream:
for event in stream:
if event.type == "content_block_delta" and event.delta.type == "text_delta":
yield f"data: {json.dumps({'text': event.delta.text})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
When to put an API layer in front of this
This pattern works well for a single app calling Claude directly with an Anthropic key. It gets more complicated once you have multiple services, need per-team usage limits, or want to track token spend without building your own metering. That's the gap SubToAPI fills: it turns your existing Claude access into an HTTPS API with its own sub_live_... keys, so your Flask app (or any other service) calls https://api.subtoapi.app/v1/messages with standard streaming support, and usage/billing metadata comes back without you having to build logging and quota tracking yourself. The request and streaming shape matches what's shown above — see the streaming docs and messages reference for exact payloads, or the quickstart to get an API key from the signup page.
FAQ
Does the Claude API support streaming out of the box? Yes. Set "stream": true (or use the SDK's .stream() method) and Anthropic returns server-sent events with incremental content deltas, tool-use events, and a final message-stop event.
Why does my Flask app buffer the stream instead of sending it live? Usually a proxy, compression middleware, or a sync Gunicorn worker is buffering the response. Disable proxy_buffering, skip gzip on the streaming route, and use threaded or async workers.
Can I stream Claude responses without managing my own Anthropic API key? Yes — services like SubToAPI let you stream through a standard API key and dashboard without handling Anthropic account setup, billing, or rate-limit logic yourself.