Server-Sent Events
Push server-to-client updates over a persistent HTTP connection with Server-Sent Events (SSE).
Recipe
Quick-reference recipe card - copy-paste ready.
import express from "express";
const app = express();
app.get("/events", (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
req.on("close", () => clearInterval(interval));
});When to reach for this: Live feeds, notifications, progress updates, and log streaming where only the server sends data.
Working Example
import express from "express";
const app = express();
const clients = new Set<import("node:http").ServerResponse>();
app.get("/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
});
res.write(": connected\n\n"); // comment line keeps connection alive
clients.add(res);
req.on("close", () => {
clients.delete(res);
});
});
function broadcast(event: string, data: unknown) {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of clients) {
client.write(message);
}
}
// Trigger from elsewhere in the app
app.post("/notify", express.json(), (req, res) => {
broadcast("notification", req.body);
res.json({ sent: clients.size });
});Client (browser):
const source = new EventSource("/events");
source.addEventListener("notification", (e) => {
console.log(JSON.parse(e.data));
});
source.onerror = () => console.log("SSE reconnecting...");What this demonstrates:
- SSE response headers including nginx buffering disable
- Named events with
event:field - Client set for broadcast fanout
- Cleanup on client disconnect
- Browser
EventSourcewith auto-reconnect
Deep Dive
How It Works
- SSE is a long-lived HTTP response with
Content-Type: text/event-stream - Messages are text lines:
data: ...\n\n(double newline terminates message) - Optional fields:
event:,id:,retry: - Browser
EventSourcereconnects automatically withLast-Event-ID
SSE Message Format
event: price-update
id: 42
data: {"symbol": "AAPL", "price": 182.50}
nginx Configuration
location /events {
proxy_pass http://node_backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}Gotchas
- nginx buffering SSE - clients see delayed or batched events. Fix:
proxy_buffering offandX-Accel-Buffering: no. - Not cleaning up on disconnect - memory leak from stale response objects. Fix: remove from client set on
close. - Writing to closed response - throws
ERR_STREAM_DESTROYED. Fix: checkres.writableEndedbefore write. - No auth on SSE endpoint - anyone can subscribe. Fix: validate cookie/token on initial GET (EventSource supports cookies).
- Broadcasting to all clients without limit - CPU spikes. Fix: rate-limit fanout; use rooms/channels.
- Using SSE for bidirectional - client cannot send over SSE. Fix: client sends via normal HTTP POST; server pushes via SSE.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| WebSocket | Bidirectional messaging | Server-only push |
| Long-polling | Very old proxy constraints | Modern infrastructure |
| Socket.IO | Need rooms and fallback | Simple one-way push |
| Webhook to client | Not applicable (no server push) | Real-time UI updates |
FAQs
SSE or WebSocket for live dashboards?
SSE. Dashboards are server-to-client. SSE is simpler, HTTP-friendly, and auto-reconnects.
How many SSE clients per Node process?
Similar to WebSocket: 10k-50k idle connections. Active broadcast reduces capacity.
Does SSE work through Cloudflare?
Yes, but check timeout limits. Cloudflare may close long-lived connections after 100 seconds on free plan.
How do I authenticate SSE?
Cookie-based auth works with EventSource. For token auth, pass as query param (less secure) or use fetch-based SSE polyfill.
Can I use SSE with Fastify?
Yes via reply.raw for direct stream write. Or use @fastify/sse plugin.
What is the retry field?
retry: 5000\n\n tells the browser to wait 5 seconds before reconnecting after disconnect.
How do I scale SSE across instances?
Redis pub/sub: each instance subscribes and writes to its local SSE clients. See Scaling Real-Time.
Does HTTP/2 affect SSE?
SSE works over HTTP/2. Multiplexing helps when the same connection serves other requests.
Related
- Real-Time Basics - SSE vs WebSocket
- Scaling Real-Time - multi-instance SSE
- HTTP/2 & HTTP/3 Considerations - proxy config
- Keep-Alive & Connection Limits - connection tuning
- Real-Time Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.