Server Sent Events

Medium

What is SSE?

  1. One way. Data flows server to browser only; the client cannot send on this channel.
  2. One connection. One normal HTTP request, kept open. The server writes many messages down it.
  3. Built in. The browser's EventSource opens the stream and parses messages for you.

Step through it

Click through the steps to watch one HTTP connection open, stay open, and stream live prices. Push your own updates or drop the connection to see SSE reconnect on its own.

SYSTEM DESIGN · FRONTENDServer-Sent Events
idle
Overview
app.market.ioCLIENT
Live Prices○ offline
AAPL$189.42▲ 0.00%
META$498.20▲ 0.00%
GOOGL$178.34▲ 0.00%
open requestclient to server
event streamserver to client
prices.apiSERVER
WIRE ▸ OUTGOING EVENTS
A live stream over one HTTP connectionServer-Sent Events (SSE) let a server push a continuous stream of updates to the browser — over a single, ordinary HTTP connection that it holds open. Step through to watch a live price dashboard stay in sync.
client.js
const es = new EventSource('/prices')
 
es.onmessage = (e) => {
const tick = JSON.parse(e.data)
updateDashboard(tick)
}
 
es.onerror = () => {
// dropped? the browser retries
// and resends Last-Event-ID
}

How the connection works

Ordinary HTTP, used in a clever way:

  1. The browser calls new EventSource('/prices'), a normal HTTP GET.
  2. The server replies with Content-Type: text/event-stream and does not close the response.
  3. It holds the connection open.
  4. On any update it writes a small text message; the browser receives it right away.

The shape of an event

Each message is plain text lines (field: value), ended by a blank line:

id: 42
event: price
data: {"symbol":"AAPL","price":189.51}
retry: 3000
↵ (blank line ends the event)
  • data: the payload, the only required field (often JSON).
  • event: an optional name; without it the type is "message".
  • id: a cursor the browser remembers and replays on reconnect.
  • retry: reconnect delay in milliseconds.

Client

const source = new EventSource('/prices');
// Plain messages (no event name)
source.onmessage = (event) =>
updateUI(JSON.parse(event.data));
// A named event
source.addEventListener('price', (event) =>
updateUI(JSON.parse(event.data))
);
// The browser retries on its own
source.onerror = () => console.log('Lost. Retrying...');
source.close(); // stop when done

Server

app.get('/prices', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
let id = 0;
const timer = setInterval(() => {
id++;
res.write(`id: ${id}\n`);
res.write(`event: price\n`);
res.write(`data: ${JSON.stringify(getPrice())}\n\n`); // blank line ends it
}, 1000);
req.on('close', () => clearInterval(timer)); // clean up on disconnect
});

Reconnecting

Connections drop (wifi, server restarts). SSE handles it for you:

  • The browser reconnects on its own after a short wait.
  • It sends a Last-Event-ID header, the id of the last message it received.
  • The server resumes from there, so nothing is lost. You write no retry code.

When to use it

Great for

  • Live dashboards and metrics
  • Notifications and alerts
  • Activity and news feeds
  • Progress of a long job
  • Streaming AI text, token by token

Think twice if

  • You need two-way talk (chat, games): use a WebSocket.
  • You need to send binary data: SSE is text only.

One limit to remember

Over HTTP/1.1 a browser allows only about 6 open connections per domain. HTTP/2 removes this limit, so prefer it in production.

Quick quiz

Four questions. Tap an answer to check it.

In SSE, which way does data flow?
Which header marks the response as an SSE stream?
What marks the end of one SSE message?
If the connection drops, what does the browser do?