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.
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');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.