Server-Sent Events (SSE)
SSE is one-way server push over plain HTTP — auto-reconnects, supports event IDs for resumption, and is the simplest real-time protocol available.
The Problem
Applications need to push real-time updates from server to client — notifications, live feeds, progress bars, AI streaming responses — without the complexity of WebSocket or the waste of polling. SSE provides a dead-simple, HTTP-native solution that works through existing infrastructure.
Mental Model
Like a radio broadcast — the station transmits, the listener tunes in. If the signal drops, the radio auto-retunes to the same frequency and picks up where it left off. There is no talking back to the station through the radio, but that is fine — the goal is just to receive.
Architecture Diagram
How It Works
Server-Sent Events is embarrassingly simple compared to WebSocket, and that is exactly why it is powerful. The entire protocol boils down to: client opens an HTTP GET request, server responds with Content-Type: text/event-stream, and then keeps the connection open, sending text events as they happen.
There is no protocol upgrade. No binary framing. No handshake dance. Just HTTP doing what HTTP does, with the server choosing not to close the response.
The Event Stream Format
The wire format is plain text. Each event is a block of field-value pairs separated by a blank line:
data: Hello, world
data: {"type": "notification", "message": "New order received"}
id: 42
event: progress
data: {"percent": 75, "step": "Compiling assets"}
id: 43
retry: 3000
Four fields define the protocol:
data:— The event payload. Multipledata:lines are concatenated with newlines.id:— Sets the last event ID. On reconnection, the browser sends this asLast-Event-IDheader.event:— Names the event type. Without it, the event fires as"message". With it, useaddEventListener("progress", ...)instead.retry:— Tells the browser how many milliseconds to wait before reconnecting after a disconnect.
That is the entire protocol. No versioning, no extensions, no negotiation.
Client-Side: The EventSource API
The browser API is equally minimal:
const source = new EventSource('/api/events');
// Default "message" events
source.onmessage = (event) => {
console.log('Data:', event.data);
console.log('Last ID:', event.lastEventId);
};
// Named events
source.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
showToast(data.message);
});
source.addEventListener('progress', (event) => {
const { percent } = JSON.parse(event.data);
updateProgressBar(percent);
});
// Connection lifecycle
source.onopen = () => console.log('Connected');
source.onerror = (err) => {
if (source.readyState === EventSource.CONNECTING) {
console.log('Reconnecting...');
}
};
The killer feature is automatic reconnection. If the connection drops — network blip, server restart, load balancer timeout — the browser waits (using the retry interval) and reconnects, sending the Last-Event-ID header. The server can then replay missed events. Resumable streams come for free.
Server-Side Implementation
A minimal Node.js SSE server looks like this:
app.get('/api/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Disable nginx buffering
});
const lastId = parseInt(req.headers['last-event-id'] || '0');
// Replay missed events if client is reconnecting
if (lastId > 0) {
const missed = getEventsSince(lastId);
missed.forEach(e => {
res.write(`id: ${e.id}\ndata: ${JSON.stringify(e)}\n\n`);
});
}
// Send periodic heartbeat to keep connection alive
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n'); // Lines starting with : are comments
}, 15000);
// Push events as they happen
const handler = (event) => {
res.write(`id: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
};
eventBus.on('update', handler);
req.on('close', () => {
clearInterval(heartbeat);
eventBus.off('update', handler);
});
});
Note the comment line (: heartbeat). Lines starting with a colon are ignored by EventSource but keep the TCP connection alive through proxies and load balancers that would otherwise time out idle connections.
SSE and the AI Streaming Revolution
SSE was always a solid protocol, but it was overshadowed by WebSocket hype in the 2010s. Then large language models changed everything.
When ChatGPT launched, it needed to stream tokens as they were generated — waiting 10-30 seconds for a full response would be a terrible user experience. The solution was SSE. OpenAI's API uses it. Anthropic's API uses it. Every major LLM API streams via SSE.
Why SSE over WebSocket for AI streaming?
- Unidirectional fits perfectly. The user sends a prompt (regular POST request), and the server streams back tokens. There is no back-and-forth during generation.
- HTTP semantics are preserved. Standard auth headers, rate limiting, caching infrastructure, and CDN routing all work out of the box.
- Simpler error handling. If the stream fails, the client retries with standard HTTP. No WebSocket reconnection state machine needed.
// Streaming AI response with SSE (simplified)
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: userInput }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE format: "data: {token}\n\n"
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const token = line.slice(6);
if (token === '[DONE]') return;
appendToResponse(JSON.parse(token).content);
}
}
}
This pattern — fetch + ReadableStream instead of EventSource — is increasingly common because EventSource only supports GET requests. For POST-based APIs (like chat completions), the fetch streaming API serves the same text/event-stream format.
SSE vs WebSocket: When to Use Which
The decision is straightforward:
Use SSE when:
- Data flows server → client (notifications, feeds, streaming responses)
- Automatic reconnection with event ID resumption is needed
- The system must work through corporate proxies and firewalls
- HTTP/2 multiplexing is desirable (multiple streams, one connection)
- The infrastructure is HTTP-based (CDNs, load balancers, API gateways)
Use WebSocket when:
- True bidirectional communication is needed (chat, collaborative editing, gaming)
- Binary data needs to be sent efficiently
- Sub-millisecond latency in both directions matters
- The client sends as much data as the server
The most common mistake I see in production systems is reaching for WebSocket when SSE would be simpler, more reliable, and easier to operate. A notification system does not need WebSocket. A live dashboard does not need WebSocket. An AI streaming response does not need WebSocket.
Scaling SSE Connections
Each SSE connection is a long-lived HTTP connection. At scale, this means managing thousands or millions of open connections. Here is what to know:
Connection limits: HTTP/1.1 browsers limit 6 connections per domain. Opening 3 SSE streams leaves only 3 connections for regular API calls. HTTP/2 eliminates this problem — all streams multiplex over a single TCP connection.
Server memory: Each connection consumes a file descriptor and some memory. Node.js handles this well with its event loop. A single Node process can hold 100K+ idle SSE connections. Go is even better with goroutines.
Load balancer considerations: Most load balancers (ALB, nginx) have idle connection timeouts. Send heartbeat comments every 15-30 seconds to keep connections alive. Configure the load balancer's idle timeout to be longer than the heartbeat interval.
Horizontal scaling: With multiple server instances behind a load balancer, a client's SSE connection lands on one server. If another server generates an event, a pub/sub layer (Redis, NATS, Kafka) is needed to fan events to all servers holding connections.
Client A ──SSE──→ Server 1 ←──Redis Pub/Sub──→ Server 2 ←──SSE── Client B
↑
Event produced
on Server 2
SSE is not a silver bullet. But for the vast majority of real-time use cases — especially in the age of AI streaming — it is the right tool. It is simpler to implement, simpler to debug, simpler to operate, and it works with every piece of existing HTTP infrastructure.
Key Points
- •SSE uses plain HTTP — no protocol upgrade, no special handshake. Any HTTP server, proxy, or CDN can serve it without configuration changes.
- •The EventSource API automatically reconnects on disconnect with exponential backoff, sending the Last-Event-ID header so the server can resume.
- •SSE supports named event types, enabling multiplexed data streams (notifications, progress, updates) over a single connection.
- •SSE is making a major comeback because of LLM streaming — ChatGPT, Claude, and most AI APIs stream token-by-token responses via SSE.
- •Unlike WebSocket, SSE works through HTTP/2 multiplexing, meaning multiple SSE streams share a single TCP connection without head-of-line blocking.
Key Components
| Component | Role |
|---|---|
| EventSource API | Browser-native JavaScript API that opens a persistent HTTP connection and parses the text/event-stream format |
| Event Stream Format | Simple text protocol with data:, event:, id:, and retry: fields separated by double newlines |
| Last-Event-ID | Header sent on reconnection so the server can resume the stream from where the client left off |
| Retry Field | Server-specified reconnection delay in milliseconds, controlling how quickly the client retries after a disconnect |
| Named Events | Custom event types beyond the default 'message' event, allowing multiplexed streams over a single connection |
When to Use
Use SSE when data flows primarily from server to client: live notifications, real-time dashboards, AI response streaming, build logs, stock tickers, or any scenario where the client is a consumer, not a producer. For bidirectional communication, use WebSocket instead.
Tool Comparison
| Tool | Type | Best For | Scale |
|---|---|---|---|
| Native EventSource API | Open Source | Simple browser-native SSE consumption with zero dependencies | Small-Enterprise |
| eventsource (npm polyfill) | Open Source | Node.js SSE client or adding custom headers (auth tokens) that native EventSource does not support | Small-Enterprise |
| Mercure | Open Source | SSE hub with pub/sub topics, JWT auth, and built-in reconnection handling | Medium-Enterprise |
| Pushpin | Open Source | Reverse proxy that adds SSE and WebSocket push capabilities to any REST API | Medium-Enterprise |
Debug Checklist
- Verify Content-Type header is exactly 'text/event-stream' — browsers will not activate EventSource parsing without it.
- Check for buffering proxies — nginx needs 'proxy_buffering off;' and 'X-Accel-Buffering: no' header.
- Confirm CORS headers are set if the SSE endpoint is on a different origin — Access-Control-Allow-Origin is required.
- Test Last-Event-ID header on reconnect — disconnect the client and verify the server receives the ID and resumes correctly.
- Check HTTP/1.1 connection limits — multiple SSE streams to the same domain may exhaust the 6-connection browser limit.
Common Mistakes
- Using WebSocket when only server-to-client push is needed. SSE is simpler, auto-reconnects, and works through HTTP infrastructure natively.
- Forgetting to set Content-Type to text/event-stream. Without it, browsers will not parse the stream as events.
- Running SSE behind a buffering reverse proxy (like nginx with default settings) that holds the response until the connection closes instead of streaming chunks.
- Not implementing Last-Event-ID on the server side, causing clients to miss events after reconnection.
- Ignoring the 6-connection-per-domain limit in HTTP/1.1. With HTTP/2 this is not an issue, but on HTTP/1.1, each SSE stream consumes one of those precious slots.
Real World Usage
- •ChatGPT and Claude stream AI responses token-by-token over SSE, giving users the typewriter effect without waiting for full completion.
- •GitHub uses SSE for live page updates — when a CI build finishes or a new comment appears, the page updates without polling.
- •Twitter/X used SSE for real-time timeline updates before their architecture changes.
- •Vercel uses SSE to stream build logs in real-time during deployments.
- •Stripe Dashboard uses SSE to push real-time payment notifications and webhook delivery status.