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.

Backend Engineerpythonfastapisse

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:

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

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

Return the Correct HTTP Status CodeBackend projectRepair Broken API PaginationBackend projectAdd CORS Middleware to an Express APIBackend projectBackend roadmapStep by step to hiredBackend interview questionsSTAR answersAll Backend projectsProjects hub

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 →