Streaming touches multiple layers: protocol, framework, client. Picking the right protocol upfront saves you migrating later. This post is the working comparison.
The protocols
| Direction | Protocol | Use | |
|---|---|---|---|
| SSE | Server → Client | HTTP/1.1 + EventStream | Notifications, LLM tokens, progress |
| WebSocket | Bidirectional | WS upgrade from HTTP | Chat, collaborative editing |
| gRPC streaming | Both | HTTP/2 | Typed RPC streams (services) |
| NDJSON | Server → Client | HTTP chunked | Programmatic streams |
| Long polling | Pull | HTTP | Compatibility fallback |
| WebTransport | Both | HTTP/3 (QUIC) | Future; low latency |
For most apps in 2026: SSE or WebSocket. WebTransport is emerging but not yet ubiquitous.
SSE
@app.get("/events")
async def events():
async def gen():
while True:
evt = await get_next_event()
yield f"data: {json.dumps(evt)}\n\n"
return StreamingResponse(gen(), media_type="text/event-stream")
const es = new EventSource("/events");
es.onmessage = (e) => console.log(JSON.parse(e.data));
Auto-reconnect; HTTP semantics; works through proxies. See FastAPI Streaming .
WebSocket
@app.websocket("/ws")
async def ws(websocket: WebSocket):
await websocket.accept()
async for msg in websocket.iter_json():
# process
await websocket.send_json({...})
const ws = new WebSocket("wss://example.com/ws");
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.send(JSON.stringify({...}));
Full duplex. Need reconnect logic. Different proxy semantics.
See Django Channels .
gRPC streaming
rpc StreamEvents(EventRequest) returns (stream Event);
Typed; HTTP/2; binary efficient. Best for service-to-service. For browsers: needs gRPC-Web or Connect.
See gRPC Basics 2026 and Go gRPC Streaming .
NDJSON
@app.get("/items.ndjson")
async def items():
async def gen():
async for item in db.iter_items():
yield json.dumps(item.dict()) + "\n"
return StreamingResponse(gen(), media_type="application/x-ndjson")
const resp = await fetch("/items.ndjson");
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value);
let nl;
while ((nl = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, nl);
buffer = buffer.slice(nl + 1);
if (line.trim()) console.log(JSON.parse(line));
}
}
Simple format. Great for programmatic consumers. No SSE event types or comments to handle.
When SSE wins
- One-way streams.
- Browser clients (auto-reconnect).
- HTTP-friendly (corporate proxies, CDNs).
- LLM token streaming.
- Notifications.
- Progress indicators.
When WebSocket wins
- Bidirectional (chat, editing).
- High-frequency small messages.
- Game / real-time collaboration.
When gRPC streaming wins
- Service-to-service.
- Typed contracts.
- HTTP/2 multiplexing.
- Bidi RPC.
When NDJSON wins
- Programmatic clients (CLIs, SDKs).
- Bulk export endpoints.
- Don’t need event types.
- Don’t want “data:” prefix.
Reconnect / resume
SSE clients auto-reconnect. To resume after disconnect:
[Server] sends SSE events with: "id: 12345\ndata: ...\n\n"
[Client] reconnects with: "Last-Event-ID: 12345" header
[Server] resumes from after id 12345
Server must support resume by id. Otherwise: gaps possible.
WebSocket reconnect is custom. Track last-seen sequence; on reconnect, send “resume from seq”.
Backpressure
For streaming responses where the producer is fast:
- HTTP / SSE: socket send buffer fills; framework’s send blocks; producer pauses naturally.
- WebSocket: framework usually backpressures; may need explicit backoff.
- gRPC: built-in flow control via HTTP/2.
Don’t buffer in your own goroutine / coroutine. Let the framework backpressure.
Heartbeats / keepalives
For SSE:
: keepalive\n\n
Every 15-30s. Prevents proxy idle-out.
For WebSocket: ping / pong frames; many libraries do automatically.
For gRPC: keepalive at the gRPC layer.
Auth
| Auth approach | |
|---|---|
| SSE | Same as HTTP (cookies, auth header for fetch-stream; query param for EventSource) |
| WebSocket | Cookies/auth at handshake; possibly token query param |
| gRPC | Metadata; auth interceptor |
SSE with EventSource doesn’t send custom headers; use cookies or token-as-query-param.
For browser SSE with bearer auth: use fetch + manual stream parsing instead of EventSource.
Disconnection detection
@app.get("/events")
async def events(request: Request):
async def gen():
try:
while True:
if await request.is_disconnected():
break
yield f"data: ...\n\n"
await asyncio.sleep(1)
except asyncio.CancelledError:
# cleanup
raise
return StreamingResponse(gen(), media_type="text/event-stream")
Without disconnect check: zombie generators run forever.
Proxies and infrastructure
- nginx:
proxy_buffering offfor SSE. - Cloudflare: free tier closes idle connections; Pro+ supports SSE/WS.
- AWS ALB: WebSocket and SSE both supported; configure timeout.
- GCP Load Balancer: similar.
Test through your full stack (LB → app); behavior differs from direct connection.
Common mistakes
1. SSE without keepalive
Connection dies at 60s; clients keep reconnecting. Always heartbeat.
2. WebSocket without reconnect
Wifi blip → client disconnected → app shows stale state until refresh.
3. Buffering on server
Producer pumps faster than client reads → OOM. Backpressure properly.
4. No disconnect check
Zombie streams burn LLM tokens / DB connections.
5. Mixed buffering at proxy
CDN buffers responses; SSE doesn’t stream. Explicit Cache-Control: no-store and X-Accel-Buffering: no.
What I’d ship today
For new streaming features:
- SSE for LLM token streams, notifications, progress.
- WebSocket for chat, collaborative editing.
- gRPC streaming for service-to-service.
- NDJSON for programmatic / CLI consumers.
- Heartbeats + reconnect universally.
- Disconnect detection in producers.
Read this next
- FastAPI Streaming and SSE 2026
- Django Channels & WebSockets 2026
- gRPC Basics 2026
- Go gRPC Streaming 2026
If you want my streaming reference (SSE + WS + reconnect), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .