Full-Stack Streaming Protocols: SSE, Fetch Streams & WebSockets
Developers reach for WebSockets reflexively for AI token streams, then spend weeks fighting proxy buffering, nginx timeout misconfigurations, and reconnection edge cases. This article benchmarks SSE vs chunked HTTP/2 vs WebSockets for unidirectional LLM token delivery and builds a resilient SSE client with deterministic state rehydration.
Full-Stack AI Architecture: From Streaming Foundations to Resilient Agentic Systems in Node.js & React
Full-Stack Streaming Protocols: SSE, Fetch Streams & WebSockets
The first instinct for real-time token delivery is WebSockets. It feels natural — bidirectional, persistent, real-time. Then production exposes the reality: nginx proxy buffering destroys the "real-time" experience, corporate firewalls block non-HTTP upgrade requests, and a WebSocket server managing 10,000 concurrent AI sessions requires significantly more engineering than the equivalent SSE server. Most of that complexity buys nothing for a unidirectional token stream.
This article benchmarks the three protocols, explains exactly why HTTP/2 SSE is the correct default for LLM token delivery, builds a resilient SSE client with deterministic rehydration, and wires the first end-to-end streaming path from the Workspace gateway to the React UI.
Who this is for: Full-stack engineers who have shipped WebSocket-based AI chat and hit buffering or reconnection issues, or who have used fetch streaming but never handled partial-chunk boundaries or client reconnection correctly.
1. Protocol Comparison: SSE vs Fetch Streams vs WebSockets
1.1 The Decision Matrix
| Criterion | SSE (EventSource) | Fetch ReadableStream | WebSocket |
|---|---|---|---|
| Direction | Server → Client | Server → Client | Bidirectional |
| HTTP/2 multiplexing | ✅ Native | ✅ Native | ❌ Separate TCP upgrade |
| Proxy compatibility | ✅ Standard HTTP | ✅ Standard HTTP | ⚠️ Requires proxy config |
| Buffering risk | ⚠️ Needs X-Accel-Buffering: no |
⚠️ Same | ✅ Frame-based, no buffering |
| Auto-reconnect | ✅ Built-in Last-Event-ID |
❌ Manual | ❌ Manual |
| Server state cost | Stateless per-request | Stateless per-request | Stateful connection object |
| Event framing | ✅ data: / event: / id: built-in |
❌ Manual newline framing | ❌ Manual framing |
| Correct choice for LLM tokens | ✅ Yes | ✅ Yes (when you need fine control) | ❌ Overkill |
WebSockets are the right choice when the client also needs to send data mid-stream — for example, streaming audio input to a speech model while receiving text output simultaneously. For unidirectional LLM token delivery, SSE is simpler, more proxy-compatible, and requires zero additional server state.
1.2 The Proxy Buffering Problem
The single most common production failure with SSE is reverse proxy buffering. nginx, AWS ALB, and Cloudflare all buffer HTTP responses by default to optimise throughput. For SSE, this means the client receives zero tokens until the buffer fills — destroying the streaming experience.
2. Server-Side SSE Implementation
2.1 The SSE Wire Format
SSE is a simple text protocol over a persistent HTTP connection:
- Each field (
data:,id:,event:) ends with\n - Each event ends with a blank line (
\n\n) - The
id:field setsevent.lastEventId— the browser sends this asLast-Event-IDheader on reconnect
2.2 Production SSE Gateway Route
3. Client-Side SSE with Fetch (Not EventSource)
3.1 Why Fetch Instead of EventSource
The native EventSource API has two critical limitations for AI streaming:
- It only supports GET requests — you cannot send a JSON body with the conversation history
- It does not support custom request headers — you cannot send
Authorization: Bearer
The solution is to use fetch() with ReadableStream parsing, which gives full control while retaining SSE semantics.
3.2 Reconnection with Deterministic State Rehydration
4. The Fetch ReadableStream Alternative (Fine-Grained Control)
For cases where you need token-level processing before displaying — syntax highlighting, citation extraction, partial JSON accumulation — raw ReadableStream gives more control than SSE event framing:
Summary
| Decision | Rule |
|---|---|
| Protocol choice | HTTP/2 SSE for unidirectional LLM tokens; WebSockets only when client sends mid-stream audio/data |
| Proxy buffering | Always set X-Accel-Buffering: no and proxy_buffering off for SSE routes |
| EventSource vs fetch | Use fetch() for SSE — EventSource cannot send POST bodies or auth headers |
| TCP chunk boundary | Buffer SSE events until \n\n before parsing — a single read() can split an event |
| Reconnection | Store lastEventId persistently; send Last-Event-ID header on reconnect to resume without duplication |
What's Next
Part 5 moves to the server side of the stream. We implement production-safe TransformStream pipelines with
highWaterMarkbackpressure tuning and thereq.on('close')→AbortController.abort()chain that prevents orphaned generation loops from billing for disconnected clients.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.