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.
data: The event's actual payload. Multipledata:lines are joined with newlines into a single value.event: The event's name (type). Named, the client receives it viaaddEventListener('thatName', ...); omitted, it's the defaultmessageevent.id: The event's identifier. The browser remembers the lastidand reports it on reconnect, so it can resume from where it left off.retry: How long (in milliseconds) to wait before reconnecting after a drop. It lets the server control the reconnection interval.- (A line starting with a colon
: commentis a comment, commonly used as a keep-alive heartbeat.)
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.