Streaming touches multiple layers: protocol, framework, client. Picking the right protocol upfront saves you migrating later. This post is the working comparison.

The protocols

DirectionProtocolUse
SSEServer → ClientHTTP/1.1 + EventStreamNotifications, LLM tokens, progress
WebSocketBidirectionalWS upgrade from HTTPChat, collaborative editing
gRPC streamingBothHTTP/2Typed RPC streams (services)
NDJSONServer → ClientHTTP chunkedProgrammatic streams
Long pollingPullHTTPCompatibility fallback
WebTransportBothHTTP/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
SSESame as HTTP (cookies, auth header for fetch-stream; query param for EventSource)
WebSocketCookies/auth at handshake; possibly token query param
gRPCMetadata; 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 off for 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

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 .