JavaScript Server-Sent Events Table
| Piece | What it does | Field note |
|---|---|---|
new EventSource(url) | Subscribe to a stream | Plain HTTP GET, text/event-stream - no protocol upgrade |
onmessage | Default events | data: lines parse into e.data - the simple channel |
Custom event names | Typed channels | event: price then addEventListener('price') - routed by name |
Auto-reconnect | Built in | Browser retries on drop - the killer feature over raw WS |
Last-Event-ID header | Resume position | Server reads it and replays missed events - resumability free |
id: / retry: / event: | The line protocol | One text format, curl-debuggable - no client library needed |
CORS applies | WithCredentials | Same rules as fetch - the server sends Access-Control headers |
SSE vs WebSocket | One-way vs duplex | Server-push-only: SSE simpler + resumable; client must talk: WS |
Server-Sent Events is server push over PLAIN HTTP: EventSource opens a GET, the server holds it open writing text/event-stream lines, and the browser parses the data/event/id/retry fields. No protocol upgrade, no client library - the response is curl-debuggable text.
Bottom line: the feature that decides most debates is AUTO-RECONNECT with Last-Event-ID - when the connection drops, the browser retries and tells the server where it stopped, giving resumable delivery that WebSocket makes you build by hand. One-way server push means SSE; the client must talk mid-stream means WebSocket.
The honest part: SSE rides normal HTTP infrastructure - proxies, load balancers and CDNs understand it - but the same HTTP adds buffering hazards (compression middleware can hold chunks) and the connection-per-client cost that at scale wants a dedicated push architecture anyway.
How to use
- Subscribe in three lines: const es = new EventSource('/feed'); es.onmessage = e => render(e.data) - no headers, no handshake, plain GET semantics.
- Type your channels: server writes event: price before data: โ client adds es.addEventListener('price', ...) - one connection, many routed streams.
- Make it resumable: send id: <seq> per event, and on reconnect read the Last-Event-ID header to replay what the client missed - resumability without a client line of code.
Frequently asked questions
When is SSE the right choice versus WebSocket?
When the flow is one-directional. SSE is server-push-only over plain HTTP: simpler server code (write lines to a response), automatic reconnection with resume position, proxy/CDN-friendly, and debuggable with curl. WebSocket is full-duplex: the client and server both push mid-stream, binary frames, sub-message latency - needed for chat, games and collaborative editing. The decision line: 'does the CLIENT need to send messages on the same connection?' If no (news feeds, notifications, live dashboards, AI streaming responses), SSE wins on simplicity and resilience; if yes, WebSocket. The hybrid note: SSE for server pushes plus ordinary fetch POSTs for client actions covers most 'realtime-ish' apps without a socket.
How does Last-Event-ID resumability actually work?
The protocol's best feature, three lines server-side. Each event carries an id: line; when the connection drops, the browser reconnects AUTOMATICALLY after the retry interval and includes a Last-Event-ID header containing the last id it saw. The server reads that header and replays everything after it - the client, meanwhile, re-registers its listeners on the fresh EventSource (same object survives reconnects; same handlers keep firing). For this to be real, the server must BUFFER events (in memory or a store) keyed by id - the browser provides the transport of the resume position; the replay is your responsibility. Without buffered history, reconnect still happens but the gap is lost - better than WebSocket's silent hole, not yet delivery guarantees.
Why does my SSE stream arrive in big chunks instead of line by line?
Buffering somewhere in the chain. SSE is plain HTTP, so every intermediary applies its own rules: compression middleware (gzip buffers until a flush), proxies holding small responses, and framework response buffering. The fixes are header-and-flush discipline: Content-Type: text/event-stream, Cache-Control: no-cache, X-Accel-Buffering: no (nginx), and explicit flushes after each event in your framework. The debugging trick is exactly why SSE is loved: curl -N the endpoint and watch - if curl shows lines live but the browser lags, the buffering is in a proxy; if curl also lags, it is your server.
What are the scaling and authentication realities of SSE?
Connections are the resource. Each EventSource holds an open HTTP connection, so connection limits (six per domain in old HTTP/1.1 browsers - HTTP/2 multiplexes this away) and server memory for hanging responses are the architecture's core cost - at thousands of concurrent clients you want an event-bus backend publishing to all held responses, not per-request logic. Authentication is refreshingly ordinary: it is a GET - cookies ride automatically same-origin, and cross-origin uses withCredentials plus CORS headers. The one gap: EventSource cannot set custom headers, so Authorization-header auth needs a cookie session or a one-time ticket query param - a constraint that shapes auth design before the first event flies.