Server Sent Events
Server-Sent Events (SSE) patterns for efficient unidirectional real-time streaming from server to client
You are an expert in Server-Sent Events (SSE) for building real-time applications with server-to-client streaming. ## Key Points - `data:` — the event payload (can span multiple lines) - `event:` — a named event type (defaults to `"message"`) - `id:` — an event identifier for resumption - `retry:` — reconnection delay in milliseconds - **Use SSE when communication is primarily server-to-client** — it is simpler than WebSockets and works through all HTTP infrastructure without special configuration. - **Always include event IDs** so clients can resume after disconnection without missing events. - **Set `retry:` to control reconnection timing** — the default varies by browser. Send `retry: 3000\n\n` to set a 3-second delay. - **Disable response buffering** at every layer — set `X-Accel-Buffering: no` for Nginx, `Cache-Control: no-cache`, and disable any compression for the event stream. - **Use named events** to multiplex different data types over a single connection rather than opening multiple EventSource instances. - **Send periodic comments** (lines starting with `:`) as heartbeats to keep the connection alive through proxies and load balancers. - **Missing `Content-Type` header** — if the server does not respond with `text/event-stream`, the browser rejects the connection silently. - **Proxy buffering** — reverse proxies like Nginx buffer responses by default, causing events to arrive in bursts. Disable buffering explicitly.
skilldb get websocket-skills/Server Sent EventsFull skill: 266 linesServer-Sent Events — WebSockets & Real-Time
You are an expert in Server-Sent Events (SSE) for building real-time applications with server-to-client streaming.
Overview
Server-Sent Events (SSE) is a standard for pushing updates from a server to a browser client over a single, long-lived HTTP connection. Unlike WebSockets, SSE is unidirectional (server to client only), uses plain HTTP, and includes built-in reconnection and event ID tracking. SSE is ideal for live feeds, notifications, progress updates, and any scenario where the client primarily consumes data.
Core Concepts
The SSE Protocol
SSE uses a simple text-based protocol over HTTP. The server responds with Content-Type: text/event-stream and sends events as newline-delimited text blocks. Each event can have:
data:— the event payload (can span multiple lines)event:— a named event type (defaults to"message")id:— an event identifier for resumptionretry:— reconnection delay in milliseconds
Events are separated by a blank line (\n\n).
Automatic Reconnection
The EventSource API automatically reconnects when the connection drops. On reconnect, it sends the Last-Event-ID header with the last received id, allowing the server to resume from where the client left off.
Differences from WebSockets
| Feature | SSE | WebSocket |
|---|---|---|
| Direction | Server to client | Bidirectional |
| Protocol | HTTP | WebSocket (ws/wss) |
| Reconnection | Built-in | Manual |
| Binary data | No (text only) | Yes |
| Browser support | All modern browsers | All modern browsers |
| Proxy friendliness | Excellent | Sometimes problematic |
Implementation Patterns
Basic Server (Node.js / Express)
import express from 'express';
const app = express();
app.get('/events', (req, res) => {
// Set SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Disable nginx buffering
});
// Send initial connection event
res.write('event: connected\ndata: {"status":"ok"}\n\n');
// Send periodic updates
const intervalId = setInterval(() => {
const data = JSON.stringify({ time: new Date().toISOString(), value: Math.random() });
res.write(`data: ${data}\n\n`);
}, 1000);
// Clean up on client disconnect
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(3000);
Browser Client with EventSource
const source = new EventSource('/events');
// Default "message" events
source.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Message:', data);
});
// Named events
source.addEventListener('connected', (event) => {
console.log('Connected:', JSON.parse(event.data));
});
source.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
});
// Connection state
source.addEventListener('open', () => {
console.log('SSE connection opened');
});
source.addEventListener('error', (event) => {
if (source.readyState === EventSource.CLOSED) {
console.log('SSE connection closed permanently');
} else {
console.log('SSE reconnecting...');
}
});
Event IDs and Resumption
// Server: include event IDs
let eventCounter = 0;
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// Resume from last event if reconnecting
const lastId = parseInt(req.headers['last-event-id'] || '0', 10);
console.log(`Client reconnected, last event ID: ${lastId}`);
// Replay missed events from a buffer or database
const missedEvents = getEventsSince(lastId);
for (const event of missedEvents) {
res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
}
// Continue streaming new events
const handler = (event) => {
eventCounter++;
res.write(`id: ${eventCounter}\ndata: ${JSON.stringify(event)}\n\n`);
};
eventEmitter.on('update', handler);
req.on('close', () => eventEmitter.off('update', handler));
});
SSE with Authentication (fetch-based)
// EventSource does not support custom headers.
// Use fetch + ReadableStream for authenticated SSE.
async function connectSSE(url, token) {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error(`SSE failed: ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer
let eventType = 'message';
let data = '';
for (const line of lines) {
if (line.startsWith('event: ')) {
eventType = line.slice(7);
} else if (line.startsWith('data: ')) {
data += line.slice(6);
} else if (line === '') {
if (data) {
handleEvent(eventType, JSON.parse(data));
eventType = 'message';
data = '';
}
}
}
}
}
Multi-Channel SSE
// Server: subscribe to multiple channels via query params
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const channels = (req.query.channels || '').split(',').filter(Boolean);
const handlers = channels.map((channel) => {
const handler = (data) => {
res.write(`event: ${channel}\ndata: ${JSON.stringify(data)}\n\n`);
};
pubsub.subscribe(channel, handler);
return { channel, handler };
});
req.on('close', () => {
handlers.forEach(({ channel, handler }) => {
pubsub.unsubscribe(channel, handler);
});
});
});
// Client
const source = new EventSource('/events?channels=orders,inventory,alerts');
source.addEventListener('orders', (e) => updateOrders(JSON.parse(e.data)));
source.addEventListener('inventory', (e) => updateInventory(JSON.parse(e.data)));
source.addEventListener('alerts', (e) => showAlert(JSON.parse(e.data)));
Best Practices
- Use SSE when communication is primarily server-to-client — it is simpler than WebSockets and works through all HTTP infrastructure without special configuration.
- Always include event IDs so clients can resume after disconnection without missing events.
- Set
retry:to control reconnection timing — the default varies by browser. Sendretry: 3000\n\nto set a 3-second delay. - Disable response buffering at every layer — set
X-Accel-Buffering: nofor Nginx,Cache-Control: no-cache, and disable any compression for the event stream. - Use named events to multiplex different data types over a single connection rather than opening multiple EventSource instances.
- Send periodic comments (lines starting with
:) as heartbeats to keep the connection alive through proxies and load balancers.
Common Pitfalls
- Browser connection limit — browsers limit concurrent HTTP connections per domain (typically 6 for HTTP/1.1). SSE connections count toward this limit. Use HTTP/2 to avoid this, as it multiplexes streams over a single connection.
- Missing
Content-Typeheader — if the server does not respond withtext/event-stream, the browser rejects the connection silently. - Proxy buffering — reverse proxies like Nginx buffer responses by default, causing events to arrive in bursts. Disable buffering explicitly.
- Not cleaning up on disconnect — failing to remove event listeners or clear intervals when
req.on('close')fires leads to memory leaks and errors from writing to closed streams. - No authentication story —
EventSourcedoes not support custom headers. Use cookies, query-string tokens, or a fetch-based approach for authenticated streams. - Assuming ordered delivery across reconnects — if the server does not track event IDs and replay missed events, clients will have gaps after reconnection.
Core Philosophy
SSE is the right tool when communication is primarily server-to-client. It is simpler than WebSockets, uses plain HTTP that works through every proxy and firewall, and includes built-in reconnection with event ID resumption — features you would have to implement manually with WebSockets. If your use case is live feeds, notifications, progress updates, or real-time dashboards where the client only consumes data, SSE should be your default choice.
Event IDs are what make SSE reliable. By including an id: field with every event, you enable the browser's automatic reconnection to resume from the last received event. When the connection drops and reconnects, the browser sends the Last-Event-ID header, and your server can replay only the missed events. Without event IDs, every reconnection loses an unknown number of events.
SSE shines on HTTP/2. The traditional limitation of SSE — the 6 concurrent connection limit per domain on HTTP/1.1 — is eliminated by HTTP/2's stream multiplexing. On HTTP/2, SSE connections share a single TCP connection, making it practical to open multiple event streams to different endpoints without hitting browser limits.
Anti-Patterns
-
Opening multiple EventSource connections on HTTP/1.1 — each SSE connection counts toward the browser's 6-connection-per-domain limit, starving other requests; use HTTP/2 or multiplex event types over a single connection with named events.
-
Not disabling response buffering — reverse proxies like Nginx buffer responses by default, causing events to arrive in batches instead of streaming; set
X-Accel-Buffering: noand disable compression for the event stream. -
Not cleaning up on client disconnect — failing to remove event listeners or clear intervals when
req.on('close')fires causes memory leaks and errors from writing to closed response streams. -
Using SSE for bidirectional communication — SSE is server-to-client only; attempting to send data back through the same connection is not possible; use SSE for the downstream and regular HTTP requests for the upstream.
-
Not including event IDs for resumption — omitting the
id:field means every reconnection starts from scratch with no way to replay missed events, creating data gaps the user may never notice.
Install this skill directly: skilldb add websocket-skills
Related Skills
Chat Rooms
Chat room architecture covering message routing, history, moderation, and scalable room-based real-time systems
Collaborative Editing
Real-time collaborative editing with CRDTs and Operational Transform for conflict-free concurrent document editing
Presence
User presence system design for tracking online/offline status, typing indicators, and activity in real-time apps
Reconnection
Reconnection and offline resilience patterns for WebSocket apps including retry strategies and state synchronization
Scaling Websockets
Scaling WebSocket applications with Redis pub/sub, sticky sessions, horizontal scaling, and load balancing strategies
Socket Io
Socket.IO patterns for event-driven real-time communication with automatic reconnection and room management