Server-Sent Events & Streaming

text/event-stream and EventSource, the event/data/id/retry fields, automatic reconnection, SSE vs WebSocket vs long-polling, and chunked transfer.

Sometimes a server needs to push data to the client in real time on the web — notifications, progress, LLM token streaming, live feeds. Server-Sent Events (SSE) is the standard for that: a one-way (server→client) stream where a single HTTP response is held open and the server keeps writing text events into it. It runs over ordinary HTTP with no separate protocol, so it's simple to implement and friendly to proxies and firewalls.

The text/event-stream format

An SSE response carries Content-Type: text/event-stream, and its body follows a specific text format. Each event is a group of field: value lines, and a single blank line marks the end of an event. The connection stays open, and the server writes more as new events occur.

GET /stream HTTP/1.1
Host: example.com
Accept: text/event-stream

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

event: message
data: {"user":"kim","text":"hello"}
id: 101

event: message
data: multiple lines
data: are allowed
id: 102
retry: 5000

The fields: event, data, id, retry

An SSE stream uses only a few fields, each with a clear role.

EventSource and automatic reconnection

Browser clients consume SSE with the EventSource API. Its greatest strength is built-in automatic reconnection. When the connection drops, the browser waits the retry interval and reconnects on its own, sending the last received event id in the Last-Event-ID request header. If the server reads that header and resumes from the next event, gaps are filled with no lost events even on a flaky network.

const es = new EventSource('/stream');
es.addEventListener('message', (e) => {
  console.log('received:', e.data, 'id:', e.lastEventId);
});
es.onerror = () => { /* the browser retries automatically */ };

SSE vs WebSocket vs long-polling

Pick your real-time transport by the situation. Long-polling has the client send a request and the server hold the response until data appears, then answer — broadest compatibility, but each cycle reopens a request, adding overhead. SSE is a one-way stream where the server keeps pushing over a single connection, ideal when server push dominates (notifications, live updates, token streaming), with auto-reconnect and event IDs baked into the standard. WebSocket is a bidirectional persistent connection suited to interactive real time where the client also sends often (chat, games, collaborative editing), but it's a separate protocol off HTTP, so you own the infrastructure and reconnection logic. In short: SSE when the server mostly pushes; WebSocket when both sides talk actively.

Chunked transfer and gotchas

Since an SSE response's end isn't known in advance, it's sent piece by piece via Transfer-Encoding: chunked (HTTP/1.1) or a streaming body. Common traps: (1) an intermediary proxy or server that buffers the response will release events in bursts, so buffering must be off (e.g. nginx X-Accel-Buffering: no). (2) The browser's HTTP/1.1 per-domain connection cap (about 6) counts the SSE connection, so using HTTP/2+ relaxes this via multiplexing. (3) Send a periodic heartbeat via comment lines so idle connections aren't killed by middleboxes.