Server-Sent Events (SSE) is the forgotten cousin of WebSocket. It's a one-way HTTP/1.1 protocol where the server pushes text events to a browser via a long-lived text/event-stream connection. Smaller than WebSocket, simpler than gRPC, and natively reconnecting — for many dashboards it's the right answer.

Advertisement

The wire format

The protocol is dead simple: HTTP response with Content-Type: text/event-stream, then lines like data: hello followed by a blank line. Multi-line events: repeat data:. Event ID for reconnection: id: 42. Named event types: event: priceUpdate.

Browser code (5 lines)

const es = new EventSource('/stream');
es.onmessage = (e) => console.log(e.data);
es.addEventListener('priceUpdate', (e) => updateChart(JSON.parse(e.data)));
es.onerror = () => console.warn('disconnected, browser will retry');
Advertisement

Server side (FastAPI)

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio, json, time

app = FastAPI()

async def event_stream():
    while True:
        yield f'id: {int(time.time()*1000)}\nevent: priceUpdate\n'
        yield f'data: {json.dumps({"px": 24050})}\n\n'
        await asyncio.sleep(1)

@app.get('/stream')
def stream(): return StreamingResponse(event_stream(), media_type='text/event-stream')

Why pick SSE

Auto-reconnect with Last-Event-ID header is built in. Works through corporate proxies that block WebSocket. Cheaper than WebSocket on the server (HTTP/1.1 keepalive, no frame parsing). Right for: stock tickers, log tails, build progress, notifications. Wrong for: chat (need client-to-server), file upload, bidirectional games.

Gotchas

Default browser limit: 6 SSE connections per origin (HTTP/1.1 cap). Use HTTP/2 to raise to 100s. Some corporate proxies buffer the response — set X-Accel-Buffering: no for nginx. Don't forget heartbeat events every 15 sec or proxies time you out.

SSE for server-to-browser. Smaller surface, auto-reconnect, no JS framework needed. Switch to WebSocket only if you need browser-to-server messages too.