Why architecture matters here
SSE architecture matters because it's the simplest browser streaming. WebSocket needs upgrade + framing; WebTransport is newer + limited. SSE is HTTP; works everywhere; browser handles reconnect for you.
Cost is one connection per client. HTTP/2 multiplexing lets many SSE streams share a TCP connection.
Reliability from automatic reconnect + Last-Event-ID. Server can implement resume.
The architecture: every piece explained
Walk the diagram top to bottom.
Browser EventSource. `new EventSource(url)`; native API.
Server. Responds with Content-Type: text/event-stream and keeps connection open.
Streamed events. `data: {payload}\n\n` per event.
Event format. id: (event ID for reconnect), event: (type), data: (payload), retry: (reconnect delay).
Reconnect. Browser auto-reconnects on drop; sends Last-Event-ID header. Server can resume.
One-way server → client. Client can't send via SSE; use separate HTTP for control.
HTTP/2 friendly. No connection-per-stream limit; multiplex.
Firewall friendly. Standard HTTP; proxies pass.
Auth via headers or cookies. Cookies work via EventSource; custom headers require fetch-based polyfill.
LLM token streaming. Primary modern use case.
End-to-end SSE flow
Trace an SSE call. Browser: `const es = new EventSource('/chat/stream?session=abc');`
Server accepts GET, sets Content-Type: text/event-stream. Doesn't close response.
LLM generates tokens. Server writes `id: 1\ndata: {"token": "Hello"}\n\n`. Flushes.
Browser receives; onmessage fires with parsed data. UI renders "Hello".
More tokens. Server keeps sending. Browser keeps rendering.
Network drop. Browser detects; waits `retry` ms; reconnects with `Last-Event-ID: 5` header.
Server sees resume; sends messages 6 onward.
Complete: server sends `event: done\ndata: {}\n\n` and closes.
Browser onerror fires; es.close() called to prevent auto-reconnect.