System Design
What Are Server-Sent Events (SSE)?
When the server needs to stream updates to the browser - but the browser doesn't need to talk back - Server-Sent Events are the simplest, most elegant tool. Lighter than WebSockets, built right into HTTP. Here's how they work.
A lot of real-time features only need data to flow one way: from the server to the browser. A live news feed, a stock ticker, a notification stream, a progress indicator for a long-running job - in all of these, the server has a stream of updates to push, and the client just needs to receive them. The client isn't sending a constant stream back; it's watching.
For exactly this pattern, reaching for WebSockets (which give you full two-way communication) is often overkill. The simpler, purpose-built tool is Server-Sent Events (SSE) - a standard way for a server to push a continuous stream of updates to a browser over a single, long-lived HTTP connection. This guide explains what SSE is, how it works (with code), what makes it elegant, and when to choose it over the alternatives.
What SSE is
Server-Sent Events is a technology that lets a server send a stream of updates to a client over a single, persistent HTTP connection. The client opens one connection, and the server keeps it open, pushing events down it whenever it has something new - for as long as the connection lives.
The defining characteristics:
- One-directional: data flows from server to client only. The client receives a stream of events; it does not send data back over the same channel (if it needs to send something, it makes a normal separate request).
- Persistent: the connection stays open, so the server can push events continuously without the client re-asking.
- Built on plain HTTP: SSE uses ordinary HTTP, which means it works through existing web infrastructure (proxies, firewalls) without special handling, and it's simple to implement on both ends.
A useful way to picture it: SSE is like subscribing to a live radio broadcast. You tune in once (open the connection), and the station streams content to you continuously. You just listen - you don't broadcast back to the station. That one-way streaming nature is exactly SSE's shape.
sequenceDiagram
participant C as Client (browser)
participant S as Server
C->>S: open connection (EventSource)
S->>C: event: "price: 100"
S->>C: event: "price: 102"
S->>C: event: "price: 101"
Note over C,S: Server keeps pushing; client just listens
Note over C,S: If connection drops, browser auto-reconnects
This sequence diagram shows Server-Sent Events. The client opens a single connection using EventSource. The server then pushes a stream of events down that connection - price 100, price 102, price 101 - whenever it has updates. The client simply listens. If the connection drops, the browser automatically reconnects, which is a built-in feature of SSE.
How SSE works
On the client side, SSE is built into browsers through a simple interface called EventSource. You point it at a URL, and it handles opening the connection and receiving the stream of events. Here's what using it looks like:
// Open an SSE connection to the server
const events = new EventSource("/api/price-stream");
// Called every time the server pushes a new event
events.onmessage = (event) => {
console.log("New update:", event.data);
};
// Called if an error occurs (the browser will auto-reconnect)
events.onerror = (err) => {
console.log("Connection issue - browser will retry");
};
That's nearly all there is to it on the client. You open an EventSource, and onmessage fires every time the server sends an event - no polling, no re-requesting. The server pushes; the client reacts.
On the server side, SSE has a simple text-based format. The server responds with a special content type (text/event-stream) and keeps the connection open, writing events in a plain format where each event looks like:
data: price: 100
data: price: 102
Each event is just data: followed by the content, separated by a blank line. The server writes these down the open connection as things happen, and the browser parses each one and fires onmessage. The format also supports a few optional fields - an event: type (to send different kinds of events), an id: (to track position), and a retry: (to suggest reconnection timing) - but the core is this simple data: stream.
The built-in features that make SSE elegant
SSE includes a few thoughtful features out of the box that you'd otherwise have to build yourself:
Automatic reconnection. If the connection drops (network blip, server restart), the browser automatically reconnects without any code on your part. This is a significant convenience - with raw WebSockets, you typically write your own reconnection logic, but SSE handles it for you.
Event IDs and resumption. SSE can attach an ID to each event, and when the browser reconnects, it tells the server the ID of the last event it received (via a Last-Event-ID header). This lets the server resume the stream from where it left off, so the client doesn't miss events during a brief disconnection. This built-in resumption is genuinely useful for reliable streams.
Multiple event types. A single SSE stream can carry different types of events (using the event: field), so the client can listen for and handle different kinds of updates over the same connection.
These features - automatic reconnection and resumption especially - are why SSE is so pleasant for one-way streaming. The hard parts of keeping a long-lived stream reliable are largely handled for you.
Important: SSE's defining trade-off: it's simpler and lighter than WebSockets, with reconnection and resumption built in - but it's one-way only. If the server needs to stream to the client and the client doesn't need to push a constant stream back, SSE is usually the better, simpler choice. The moment you need genuine two-way interaction, you've outgrown SSE and want WebSockets.
SSE vs WebSockets
Since SSE and WebSockets are often weighed against each other, the comparison deserves to be clear:
| Server-Sent Events | WebSockets | |
|---|---|---|
| Direction | Server → client only (one-way) | Both directions (full-duplex) |
| Protocol | Plain HTTP | Upgraded WebSocket protocol |
| Auto-reconnection | Built in | You implement it |
| Resumption after drop | Built in (event IDs) | You implement it |
| Complexity | Simpler | More involved |
| Best for | One-way streams (feeds, notifications) | Interactive two-way (chat, games) |
The decision is genuinely about direction. If your feature is the server streaming updates to a client that just watches, SSE is simpler, lighter, and gives you reconnection for free. If your feature needs the client to constantly send data back too, SSE can't do it - you need WebSockets. Choosing SSE where it fits avoids the extra complexity and resource cost of WebSockets for no benefit.
When to use SSE
SSE is the right choice when:
- The data flows one way, server to client, and the client doesn't need to push a constant stream back.
- You want a continuous stream of updates - a live feed, real-time notifications, a dashboard, a stock ticker, progress updates for a long task.
- Simplicity matters - SSE is easier to implement than WebSockets and works over standard HTTP infrastructure.
- You want reconnection handled for you rather than building it yourself.
Common real uses: live news and social feeds, notification streams, real-time dashboards and monitoring displays, stock and price tickers, live sports scores, and progress updates for long-running operations. In every case, the server has a stream to push and the client is a listener.
A worked example: streaming order status
Let me make SSE concrete. Imagine an app where a user places an order, and you want to show its status updating live - "received," "preparing," "out for delivery," "delivered" - as it progresses, without refreshing.
This is a textbook SSE case: status updates flow one way (server to client), the client just watches, and you want each update to appear the moment it happens. End to end: when the user opens the tracking page, the browser opens an EventSource connection to a streaming endpoint for that order, and the server keeps it open. As the order moves through the kitchen and delivery systems, each status change triggers the server to write a new event down the open connection - data: preparing, then data: out for delivery, then data: delivered. Each arrives at the browser instantly and updates the display. At no point does the browser poll or re-request - the server simply pushes each update as it happens.
If the connection drops momentarily (the user goes through a tunnel), the browser automatically reconnects, and because SSE supports event IDs, the server can resume from the last status the client saw - so they don't miss the update that happened during the blip. The whole feature is a few lines of client code and a server writing events to an open stream. That simplicity, for a one-way live feed, is exactly why SSE exists.
The event stream format in detail
SSE's wire format is deliberately simple and human-readable. The server sends a stream of text where each message is a set of field: value lines, with a blank line marking the end of one event:
data:- the actual content. The only required field. Multipledata:lines in one event are concatenated, allowing multi-line data.event:- an optional event type name, so one stream can carry different kinds of events (apriceUpdateversus anewsAlert), and the client can listen for each type.id:- an optional identifier. The browser remembers the last ID and sends it back (asLast-Event-ID) on reconnect, enabling resumption.retry:- an optional instruction for how long to wait before reconnecting.
A small example of what the server writes:
event: status
id: 3
data: out for delivery
event: status
id: 4
data: delivered
The format's simplicity means SSE is easy to implement and debug - you can read the stream as plain text - in contrast to the more involved framing of WebSocket messages.
A real limitation: the connection limit
One practical constraint: browsers historically limited simultaneous connections to a single domain, and because each SSE stream holds a connection open, opening many SSE connections from one tab could hit that limit. The good practice is to use one SSE connection per page and multiplex different event types over it (using the event: field), rather than opening a separate stream per widget. Modern protocol improvements have eased this considerably, but the one-stream-multiple-event-types approach remains the right pattern.
SSE vs simple polling
It's worth contrasting SSE with the plain polling it improves on. Polling means the client repeatedly asks "anything new?" on a timer - wasting requests when there's nothing, and adding latency equal to the interval. SSE replaces that with a single open connection over which the server pushes updates the instant they occur: no wasted empty requests, no interval latency. For a stream of server-side updates, SSE is strictly better than polling on both efficiency and freshness - the only reason to prefer polling is if updates are so rare that holding a connection open isn't worth it, or if your infrastructure can't hold persistent connections.
Implementing the server side
While the client side is just EventSource, the server has a couple of requirements: it must respond with the content type text/event-stream, keep the connection open rather than closing after the first response, and write each event in the simple format as it occurs, flushing so it's sent immediately rather than buffered. At scale, the same persistent-connection cost that affects WebSockets applies to SSE - each open stream consumes a connection and some resources for its lifetime. For a modest number of streams this is trivial; for millions, it requires the same connection-management infrastructure large real-time systems build. SSE is simpler than WebSockets to implement, but it shares the operational reality that persistent connections aren't free.
One more characteristic: SSE is designed for text (commonly JSON encoded as text), which is perfect for the structured updates most feeds need. If you specifically need to stream binary data, SSE isn't the right tool - that's where WebSockets (which support binary messages) fit better. For the overwhelming majority of real-time use cases - feeds, notifications, status, metrics - SSE's text orientation is exactly right.
SSE in practice: a few patterns
A handful of patterns recur when working with SSE. Heartbeats: servers often send a periodic comment line (a line starting with :) as a keep-alive, so the connection isn't closed by intermediaries that drop idle connections - the same "keep the connection alive" concern WebSockets address with ping/pong. Filtering per client: since each client opens its own connection (often to a URL that identifies what they care about), the server streams only the events relevant to that client, rather than broadcasting everything. Closing cleanly: when there are no more updates to send (the order was delivered, the job finished), the server can close the stream, and the client can choose not to reconnect. These small patterns - heartbeats, per-client filtering, clean closing - are what make SSE robust in production, and they're simpler to get right than their WebSocket equivalents because the one-way model has fewer moving parts.
When NOT to use SSE
- When you need two-way communication. If the client must continuously send data to the server (chat, multiplayer games, collaborative editing), SSE's one-way nature makes it the wrong tool - use WebSockets.
- When updates are rare. If the server pushes something only occasionally, a persistent connection may be unnecessary overhead, and simple polling could suffice.
- For binary data. SSE is designed for text-based event streams; for streaming binary data, other approaches fit better.
Interview: SSE is the answer that shows restraint and precision in a real-time design question. When a feature is one-way server-to-client - a live feed, a notification stream - proposing SSE instead of reflexively reaching for WebSockets signals maturity: "This only needs the server to push updates to the client, so SSE is simpler than WebSockets, works over plain HTTP, and gives me automatic reconnection for free." Knowing when the lighter tool is the right tool is exactly the judgment interviewers look for.
The bottom line
Server-Sent Events let a server stream a continuous flow of updates to a browser over a single, long-lived HTTP connection - one-way, server to client. It's built into browsers via the simple EventSource interface, uses a plain text event format, and includes automatic reconnection and event-resumption out of the box. It's lighter and simpler than WebSockets, and it's the right tool whenever the server needs to push a stream of updates to a client that only needs to listen.
The decision between SSE and WebSockets comes down to direction: one-way streaming → SSE; two-way interaction → WebSockets. Reach for the simpler tool when it fits. We cover real-time communication and choosing the right technique across System Design Fundamentals.
Go deeper
- Server-sent events (MDN) - the practical guide to using SSE.
- EventSource (MDN) - the client-side interface reference.
- Server-sent events (WHATWG HTML spec) - the canonical specification.
- Server-sent events (Wikipedia) - the concept and its context.