Server-sent events

On this page 7

Server-sent events (SSE) are a web standard that lets a server push a stream of text messages to the browser over a single, long-lived HTTP response. Among web APIs, SSE is the simplest way to deliver real-time updates in one direction, from server to client, using the browser’s built-in EventSource interface and the text/event-stream media type.

SSE is specified in the HTML Living Standard and works with ordinary HTTP infrastructure. Unlike WebSocket, it does not switch protocols, and unlike polling, it does not repeat requests: the connection stays open and the server writes new events whenever it has something to say, while the browser handles automatic reconnection and event replay on its own.

How server-sent events work

The client opens a stream with new EventSource(url), which sends a normal GET request with the header Accept: text/event-stream. The server answers with status 200 and the content type text/event-stream, then keeps the response open and writes events as UTF-8 text. Each event consists of one or more field lines and ends with a blank line.

The browser parses the stream incrementally and dispatches a DOM event for every completed message. Unnamed messages fire the message event; messages with an event field fire a custom event type that the page subscribes to with addEventListener. If the connection drops, the browser waits and reconnects automatically.

The wire format defines only a handful of fields, summarized below.

FieldPurposeNotes
dataPayload of the messageSeveral data lines are joined with newlines
eventNames the event typeDefaults to message when omitted
idSets the last event IDSent back in the Last-Event-ID header on reconnect
retryReconnection delay in millisecondsBrowsers default to a few seconds
Comment lineLine starting with a colonIgnored by the parser, useful as a heartbeat
Fields of the event stream format

Reconnection, event IDs and delivery guarantees

The built-in reconnection mechanism is the main practical advantage of SSE. When the stream ends or the network fails, EventSource reopens the connection after the retry delay and sends the Last-Event-ID header containing the most recent id it received. A server that stores recent events can resume exactly where the client left off.

This gives at-least-once delivery only if the server implements replay. The protocol itself keeps no buffer; if events are not stored by ID, anything published during the outage is lost. A server can stop reconnection permanently by responding with HTTP 204 No Content or with a non-stream content type.

Idle connections are a frequent operational issue. Proxies and load balancers often close responses that stay silent for 30 to 120 seconds, so servers usually send a comment line as a heartbeat every 15 to 30 seconds and disable response buffering for the stream.

Server-sent events compared with other real-time techniques

SSE fills the space between simple polling and a full bidirectional WebSocket channel. The right choice depends on message direction, data format and the infrastructure in between.

TechniqueDirectionData formatMain trade-off
Server-sent eventsServer to clientUTF-8 textSimple and HTTP-native, but one-way
WebSocketBoth directionsText or binaryFull duplex, but needs its own protocol handling
Long pollingServer to clientAnyWorks everywhere, but wastes requests
Web pushServer to deviceEncrypted payloadReaches closed tabs, but needs a push service
WebRTC data channelPeer to peerText or binaryLow latency, but complex signaling
Real-time delivery options in the browser

Client-to-server messages in an SSE design simply use ordinary fetch or form requests. For many dashboards, notifications and feeds this split is cleaner than a WebSocket, because every request remains a normal HTTP request that existing authentication, logging and caching layers already understand.

Connection limits and HTTP versions

Each open stream occupies a connection, which matters under HTTP/1.1, where browsers allow about six connections per origin. Several tabs with streams can exhaust that budget and block other requests to the same host. Under HTTP/2 and HTTP/3, streams are multiplexed and the practical limit rises to the negotiated number of concurrent streams, typically 100 or more.

TransportConcurrent streamsPractical consequence
HTTP/1.1About 6 per originShare one stream across tabs or use a separate subdomain
HTTP/2Usually 100 or moreMany streams per tab are feasible
HTTP/3Negotiated, usually 100 or moreNo head-of-line blocking between streams
Effect of the transport on SSE behavior

On the server, every client holds an open response, so event-driven runtimes that do not dedicate a thread per connection scale far better. Thread-per-request servers can run out of workers long before CPU or memory become the limit.

Practical choices and common pitfalls

Most SSE failures come from intermediaries and missing replay logic, not from the API itself. The following table maps typical symptoms to their causes.

SymptomLikely causeFix
Events arrive in burstsProxy or compression bufferingDisable buffering and flush after each event
Stream closes every minuteIdle timeout on a proxySend heartbeat comments
Missed messages after reconnectNo storage by event IDKeep a replay buffer keyed by id
Other requests hangHTTP/1.1 connection limitMove to HTTP/2 or share one stream
Cannot send auth headerEventSource has no custom headersUse cookies or a fetch-based stream reader
Troubleshooting server-sent event streams

EventSource supports cookies through withCredentials but not custom request headers, so token-based APIs often read the stream with fetch and a manual parser instead. Binary payloads must be encoded, for example as Base64, which adds roughly one third to the size.

The short version

Server-sent events work like a radio broadcast for a web page. The page tunes in once, and the server keeps talking whenever there is news, without the page having to ask again and again.

If the signal drops, the page tunes back in by itself and can tell the server which message it heard last. The page listens, the server speaks, and when the page needs to say something back, it uses an ordinary request.

Where it makes a difference

An online auction platform shows live bids on each item page. Thousands of visitors watch the final minutes of popular auctions, but only a few of them place bids. The team uses one SSE stream per page for price updates and normal POST requests for bids, served over HTTP/2.

Every event carries an incrementing id, and the server keeps the last five minutes of events in memory. When a visitor on a train loses signal for twenty seconds, the browser reconnects with Last-Event-ID and receives the missed bids at once. Viewers see accurate prices within a fraction of a second, and the existing HTTP load balancers and logs handle the traffic without a separate WebSocket gateway.