Streaming Inference with FastAPI and Server-Sent Events
Turn a blocking LLM call into an incremental, observable stream of tokens with FastAPI StreamingResponse and Server-Sent Events — including cancellation and backpressure.
Published on • August 3, 2026
AI Assistant

Streaming is how you turn a minutes-long model call into a visible, responsive one, and it pairs naturally with measurable, observable LLM infrastructure.
The problem: blocking calls
A normal LLM API call is a single POST that returns only when the whole answer is finished:
response = client.models.generate_content(model="gemini-2.5-pro", contents="Explain MD5 to a 5-year-old")
print(response.text)
For long answers the caller stares at a spinner. For agentic loops and chat UIs this is a terrible experience, and it makes “is it working?” unanswerable mid-generation.
Server-Sent Events (SSE): one-way, streaming text
SSE is a lightweight HTTP protocol — a single long-lived connection where the server pushes data: lines as events arrive. It is the right tool when the server pushes to one browser (one-way). It’s simpler than WebSockets and built on plain HTTP.
A token stream from the model looks like:
data: {"text": "Mult"}
data: {"text": "iple "}
data: {"text": "enc"}
...
data: [DONE]
Streaming from Gemini in Python
The Gemini Python SDK supports streaming:
from google import genai
client = genai.Client()
stream = client.models.generate_content_stream(
model="gemini-2.5-pro",
contents="Explain MD5 to a 5-year-old in 4 short lines.",
)
for chunk in stream:
print(chunk.text, end="")
chunk.text arrives progressively. Now wire that into FastAPI.
Building a FastAPI streaming endpoint
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from google import genai
import json
app = FastAPI()
client = genai.Client()
@app.post("/v1/stream")
async def stream_llm(prompt: str):
async def event_generator():
stream = client.models.generate_content_stream(
model="gemini-2.5-pro", contents=prompt
)
for chunk in stream:
# Each token becomes one SSE "data:" line
yield f"data: {json.dumps({'text': chunk.text})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
Key points:
media_type="text/event-stream"tells the client this is SSE.X-Accel-Buffering: nodisables reverse-proxy buffering (Nginx) so chunks reach the client immediately.
Handling cancellation
If the client disconnects, a generator that keeps yielding wastes tokens and money. Detect cancellation and stop the underlying stream:
import asyncio
async def event_generator():
try:
stream = await asyncio.to_thread(
client.models.generate_content_stream,
model="gemini-2.5-pro",
contents=prompt,
)
for chunk in stream:
yield f"data: {json.dumps({'text': chunk.text})}\n\n"
await asyncio.sleep(0) # let cancellation surface
except asyncio.CancelledError:
# stop early: don't keep billing for an abandoned generation
print("client disconnected, cancelling generation")
raise
Putting It All Together
The browser consumes it with EventSource:
const es = new EventSource('/v1/stream');
es.onmessage = (e) => {
if (e.data === '[DONE]') { es.close(); return; }
appendToChat(JSON.parse(e.data).text);
};
Combine with an observability wrapper (a trace_id span around the generator) — the ref’s “operate AI like production infrastructure” — so you can see tokens/s, abort rate, and cost per stream.
Conclusion & Next Steps
Streaming turns a blocking black-box call into an incremental, observable one. Next: add a text/plain fallback, surface token-count and cost metadata on the [DONE] event, and add cancellation metrics to your dashboard so you know exactly how much an aborted stream cost.
References / Sources
- FastAPI StreamingResponse. https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse
- Gemini streaming. https://ai.google.dev/gemini-api/docs/text-generation
- MDN Server-Sent Events. https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events