Instead of clients nagging “anything new yet?”, the server holds the line open and pushes each event out to everyone at once.
Imagine a newsroom where every reader keeps phoning in to ask “any breaking news?” — most calls hear “nothing yet.” That’s short-polling: cheap to build, but wasteful, and news is always stale by up to one polling interval.
With long-polling the reader phones in and the newsroom simply doesn’t hang up until there’s actually something to report. With server-sent events (SSE) the reader opens one durable line and the newsroom keeps narrating events down it as they happen — one stream, many readers. Publishing once fans out to every connected subscriber.
An SSE endpoint never finishes the response. It sets Content-Type: text/event-stream, then writes one message block at a time. Each block is a few text lines — an id:, optional event:, one or more data: lines — terminated by a blank line. The browser’s EventSource parses this stream and remembers the last id: it saw.
// SSE endpoint (server, Node-ish pseudocode)
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
res.write('retry: 3000\n\n'); // tell client how long to wait before reconnect
function send(id, payload) {
res.write(`id: ${id}\n`);
res.write(`event: notice\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`); // blank line ends the block
}
// heartbeat so proxies don't time the idle connection out
setInterval(() => res.write(': keepalive\n\n'), 15000);
When a client reconnects, the browser automatically sends the header Last-Event-ID. The server reads it and replays anything newer from a small buffer, so no events are lost across a blip.
// client (browser)
const es = new EventSource('/feed');
es.addEventListener('notice', (e) => {
console.log('event', e.lastEventId, JSON.parse(e.data));
});
// on a dropped connection the browser reconnects on its own and sends:
// Last-Event-ID: 4
// the server then replays event 5, 6, ... from its buffer.
Long-poll is the same idea without a streaming body: the handler blocks until an event arrives or a timeout fires, returns one response, and the client immediately re-requests.
// long-poll handler
async function handle(req, res) {
const since = Number(req.query.since || 0);
const event = await waitForEventAfter(since, { timeoutMs: 25000 });
if (event) res.json({ id: event.id, data: event.data });
else res.status(204).end(); // nothing yet — client re-polls
}
| Approach | Requests | Latency | Direction | Server cost |
|---|---|---|---|---|
| Short-poll | One every interval, mostly empty | Up to one interval | Request / response | Cheap per request, wasteful in bulk |
| Long-poll | One per event (plus timeouts) | Near real-time | Request / response | Holds a connection while waiting |
| SSE | One stream, stays open | Real-time push | Server → client only | One open connection + buffer per subscriber |
| WebSocket | One upgraded socket | Real-time push | Bidirectional | One open socket per subscriber, more infra |
The hidden line item for long-poll, SSE, and WebSocket is the same: every subscriber is a connection the server must keep in memory. Ten thousand idle subscribers is ten thousand held connections — pick the model whose memory and proxy behaviour your fleet can actually carry.
: keepalive heartbeats so nothing in the middle gives up on you.id: on each event and a server-side replay buffer, a reconnect silently skips whatever shipped during the gap. Always set id: and honour Last-Event-ID.A live notifications feed powers three open dashboards. Each opens EventSource('/notifications'); the server tracks the trio as connected subscribers and keeps a buffer of the last 50 events keyed by id.
A new alert arrives and is published as event id: 5. The server writes that block once to each of the three streams — a single publish fans out to all three dashboards within milliseconds. Dashboard B’s laptop sleeps and its connection drops after event 4. On wake the browser reconnects automatically and sends Last-Event-ID: 4; the server reads the header, finds events 5 onward still in its buffer, and replays them so B’s badge count matches A and C. No alert is lost, and B never had to ask “what did I miss?”
1. A client’s SSE connection drops, then reconnects. How does it avoid missing events that were published during the gap?
2. Your dashboard needs the client to send messages back to the server too, not just receive. Is SSE the right fit?