Careeroza
Streaming Responses with SSE
Why Stream
A 300-word answer can take several seconds. With streaming, the first words appear in well under a second and the user reads while the rest arrives. Browsers receive it as server-sent events (SSE): a long HTTP response made of data: ... lines separated by blank lines.
Example
Example
The SSE format itself: how the server encodes chunks and how a client parses them back.
import json
def sse(data, event=None):
lines = ([f"event: {event}"] if event else []) + [f"data: {json.dumps(data)}"]
return "\n".join(lines) + "\n\n"
chunks = ["Shoes can ", "be returned ", "within 10 days."]
stream = "".join(sse({"text": c}) for c in chunks) + sse({"output_tokens": 9}, event="done")
print(stream)
# client side: split on blank lines, read the data field
text = ""
for message in stream.strip().split("\n\n"):
fields = dict(line.split(": ", 1) for line in message.split("\n"))
payload = json.loads(fields["data"])
if fields.get("event") == "done":
print("done, tokens:", payload["output_tokens"])
else:
text += payload["text"]
print("assembled:", text)Output
data: {"text": "Shoes can "}
data: {"text": "be returned "}
data: {"text": "within 10 days."}
event: done
data: {"output_tokens": 9}
done, tokens: 9
assembled: Shoes can be returned within 10 days.Streaming Endpoint
import json
from fastapi.responses import StreamingResponse
@app.post("/chat/stream")
async def chat_stream(body: ChatIn):
async def events():
async with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=1000,
system=SYSTEM,
messages=body.history + [{"role": "user", "content": body.message}],
) as stream:
async for text in stream.text_stream:
yield f"data: {json.dumps({'text': text})}\n\n"
final = await stream.get_final_message()
yield f"event: done\ndata: {json.dumps({'output_tokens': final.usage.output_tokens})}\n\n"
return StreamingResponse(events(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})Reading the Stream in the Browser
const res = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const events = buffer.split('\n\n');
buffer = events.pop()!; // keep the incomplete part
for (const ev of events) {
const data = ev.split('\n').find((l) => l.startsWith('data: '));
if (data && !ev.startsWith('event: done')) setReply((r) => r + JSON.parse(data.slice(6)).text);
}
}Note: Proxies like Nginx buffer responses by default, which breaks streaming. The X-Accel-Buffering: no header (or proxy_buffering off;) fixes it.