Server-Sent Events
Most realtime data flows in one direction: the server knows something new and needs to tell the browser. A user's inbox count changed, a build finished, a document was updated by a colleague. For these cases, the full bidirectional channel of a WebSocket is more than necessary. Server-Sent Events gives you exactly what you need, a persistent HTTP connection over which the server can push a stream of text events, with automatic reconnection and gap recovery built into the protocol itself.
This guide follows WebSocket fundamentals and sits inside the Interactive Web cluster covering realtime frontend engineering patterns.
What you'll learn
The text/event-stream wire format
SSE has a simple, human-readable wire format that is worth understanding before touching the API, because it explains every feature the browser exposes.
Fields, blank lines, and multi-line data
# Comment lines start with a colon; the browser ignores them
: heartbeat
# Minimal event: just a data field
data: {"type":"ping"}
# Named event with an ID
id: 42
event: patch
data: {"field":"title","value":"New Title"}
# Multi-line data (reassembled by the browser)
data: {"lines":[
data: "first",
data: "second"
data: ]}
Each event is a block of field: value lines terminated by a blank line. The four meaningful fields are data, the payload; event, an optional type name; id, an event identifier; and retry, an override for the reconnection delay in milliseconds. A colon at the start of a line is a comment, which servers send periodically to prevent proxy timeouts on idle connections. Multi-line payloads use repeated data: lines; the browser joins them with newlines before firing the event.
The EventSource API
The browser exposes SSE through the EventSource interface, which manages the connection, handles reconnection, and fires DOM events.
Opening a stream and receiving events
var es = new EventSource('/api/stream', { withCredentials: true });
es.addEventListener('open', function () {
console.log('stream connected');
});
es.addEventListener('message', function (event) {
// fires for events with no 'event:' field
var payload = JSON.parse(event.data);
handleGeneric(payload);
});
es.addEventListener('error', function () {
if (es.readyState === EventSource.CLOSED) {
console.log('stream closed permanently');
}
// CONNECTING state means the browser is already reconnecting
});
// Close explicitly when no longer needed
function cleanup() { es.close(); }
withCredentials: true sends cookies with the request, which is necessary for authenticated streams. The message event fires for any event without an explicit event: field. For named events, register separate listeners as shown in the next section. The readyState is one of CONNECTING, OPEN, or CLOSED; the browser moves to CONNECTING automatically on a dropped connection unless you call close() first.
Named event types
One SSE connection can carry many logically distinct event streams by using the event: field to name them.
Routing events by type
// Listen for specific named event types
es.addEventListener('patch', function (event) {
applyPatch(JSON.parse(event.data));
});
es.addEventListener('presence', function (event) {
updatePresence(JSON.parse(event.data));
});
es.addEventListener('notification', function (event) {
showNotification(JSON.parse(event.data));
});
Named events let a single stream replace several polling endpoints or topic subscriptions, reducing connection overhead. The pattern maps naturally onto a Backbone application: a patch event triggers a model set, a presence event updates a collection, and a notification drives a view directly. The key discipline is to keep the server-side event naming stable, because renaming an event type is a breaking change for any client that has registered a listener.
Event IDs and gap recovery
Network connections drop. The id field and the Last-Event-ID header together let the stream resume exactly where it left off.
Resuming from the last seen event
# Server sends each event with a monotonically increasing ID
id: 100
event: patch
data: {"op":"replace","path":"/title","value":"Draft"}
id: 101
event: patch
data: {"op":"add","path":"/tags/-","value":"urgent"}
The browser stores the most recent id value internally. When it reconnects, it sends Last-Event-ID: 101 in the request header. The server reads that header and replays any events with IDs above 101 from its buffer or event log before resuming the live stream. This gives the client a continuous, gapless sequence without any application-level sequence tracking. The buffer on the server side is the engineering constraint: you need to retain enough history to cover the typical reconnection window, which is usually a few seconds to a few minutes depending on the use case.
Server implementation
SSE is just a long-lived HTTP response with the right content type and careful flushing. Any HTTP server can implement it.
Node.js streaming response
// Express SSE endpoint
app.get('/api/stream', function (req, res) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering
var lastId = parseInt(req.headers['last-event-id'] || '0', 10);
replayMissedEvents(lastId, res);
var unsubscribe = eventBus.subscribe(function (event) {
res.write('id: ' + event.id + '\n');
res.write('event: ' + event.type + '\n');
res.write('data: ' + JSON.stringify(event.payload) + '\n\n');
});
req.on('close', function () {
unsubscribe();
});
});
The three critical headers are Content-Type: text/event-stream, Cache-Control: no-cache, and X-Accel-Buffering: no if running behind nginx. Proxy buffering is the most common reason SSE appears to work in development but delivers events in batches in production. Each event must end with a double newline. Clean up the subscription in the close handler or the server will leak listeners for every disconnected client.
Limits and HTTP/2
SSE's one practical constraint is the HTTP/1.1 connection limit, and HTTP/2 resolves it entirely.
Six connections under HTTP/1.1, unlimited under HTTP/2
HTTP/1.1 browsers allow at most six simultaneous connections to the same origin. An SSE connection holds one of those connections open permanently, leaving five for all other requests, images, scripts, API calls. On an application with multiple SSE streams this becomes a real bottleneck. HTTP/2 multiplexes all streams over a single TCP connection, so an SSE request costs one stream out of potentially hundreds rather than one of six TCP connections. Serve SSE endpoints over HTTP/2 in any production environment with more than one concurrent stream per user. The WebSocket guide noted that WebSocket does not benefit from HTTP/2 multiplexing in the same way, which is one reason SSE is often the better choice for high-volume server-to-client feeds.
Frequently Asked Questions
What are Server-Sent Events?
Server-Sent Events is a browser API and wire protocol for receiving a stream of text events pushed by a server over a persistent HTTP connection. The browser opens one request and the server keeps the response body open, writing events as they occur.
How does SSE reconnect automatically?
When an EventSource connection drops, the browser automatically reopens it after a short delay, defaulting to three seconds. It sends the Last-Event-ID header with the id of the most recently received event, letting the server resume the stream from the correct position without the client losing any events.
Can SSE send binary data?
No. The text/event-stream format is text-only. For binary data over a server-push channel, encode as base64 in an SSE data field or switch to WebSocket, which supports binary frames natively.
How many SSE connections can a browser open?
Over HTTP/1.1, browsers limit connections to the same origin to six, and SSE uses one persistently, leaving fewer for other requests. Over HTTP/2, multiplexing removes this limit because all streams share one TCP connection. Always serve SSE endpoints over HTTP/2 in production.
Read next: the Interactive Web hub, or revisit WebSocket fundamentals for bidirectional transport comparison.
Want to drive Backbone models from a live SSE stream?
The Backbone guide covers the model layer that SSE events update in real time.
Explore the Backbone Guide →