How to Stream Tokens Over Server-Sent Events in FastAPI
Server-Sent Events let a FastAPI endpoint push each token to the browser the moment it's ready - no buffering, no WebSocket handshake, just HTTP. Here's how to wire it up with StreamingResponse and a generator.
Why streaming beats buffering for LLM output
A buffered endpoint waits for the entire response before sending anything. With an LLM generating tokens one at a time over hundreds of milliseconds, that means the user stares at a spinner until the model finishes. SSE flips this: the server pushes each token as it lands, so text appears word by word - the same UX as ChatGPT or Claude.
SSE is the simplest streaming primitive available over plain HTTP. The browser's built-in
EventSource reads it without any extra library, and it auto-reconnects on drop. No
WebSocket upgrade needed.
The SSE wire format
Each SSE message is a small text block separated by a blank line:
event: token
data: {"text": "hello"}
event: token
data: {"text": "world"}
event: done
data: {"total_tokens": 2}
The event: line is the named event type; data: is the payload (any string, usually JSON).
A double newline ends each message - that blank line is mandatory.
Implementing it in FastAPI
FastAPI's StreamingResponse wraps any Python generator and streams its output directly to the client.
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
def generate_tokens(prompt: str):
# Replace with your real LLM call that yields tokens
for word in prompt.split():
yield word
def _sse(event: str, data: dict) -> str:
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
@app.post("/api/generate")
def generate(body: dict):
prompt = body.get("prompt", "")
def stream():
count = 0
for tok in generate_tokens(prompt):
count += 1
yield _sse("token", {"text": tok})
yield _sse("done", {"total_tokens": count})
return StreamingResponse(stream(), media_type="text/event-stream")
Three things make this work:
media_type="text/event-stream"sets theContent-Typeheader that tells browsers this is SSE.- The inner
stream()generator yields strings - FastAPI never buffers them, it writes each yield straight to the socket. - The double
\n\nat the end of each_sse()call closes the event block; without it, the client never fires the event.
Testing it on the command line
# -N disables curl's own output buffering so you see events as they arrive
curl -N -X POST -H 'Content-Type: application/json' \
-d '{"prompt": "hello world from sse"}' \
http://localhost:8000/api/generate
You should see event: token lines appear one at a time, then a final event: done.
Reading SSE in the browser
// EventSource only handles GET; for POST you need fetch with a reader
const res = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "hello" }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value);
// parse complete SSE messages from buf as needed
}
For GET-based streams, new EventSource("/stream") handles reconnection and named events automatically.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Converting a buffered FastAPI endpoint into a StreamingResponse generator
- Formatting SSE event blocks correctly with named events and JSON data payloads
- Testing an SSE stream with curl -N and reading it in the browser via fetch
FAQ
How do I stream an LLM response token by token in FastAPI?
Use FastAPI's StreamingResponse with a generator function. Set media_type='text/event-stream', yield each token as an SSE block ('event: token\ndata: {"text": tok}\n\n'), and yield a final 'done' event when the generator is exhausted.
What is the correct SSE message format?
Each SSE message has an optional 'event:' line, a 'data:' line, and ends with a blank line (double newline). For example: 'event: token\ndata: {"text": "hello"}\n\n'. The double newline is required - without it the client never fires the event.
When should I use SSE instead of WebSockets for streaming?
Use SSE when the server pushes data one way to the client - LLM token output, log tailing, progress updates. SSE works over plain HTTP, auto-reconnects, and needs no handshake. Use WebSockets only when you need bidirectional real-time messaging.
What is an example of a server-sent event?
A token-by-token LLM chat reply is a classic example: the server holds one HTTP connection open and pushes data lines as each token arrives. Live notifications, progress bars, and tickers are others - one-way server-to-client streaming.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →