Implementing Server-Sent Events
1. Understanding SSE Protocol
| Aspect | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client only | Bidirectional |
| Transport | HTTP/1.1+ chunked | Upgrade to ws:// |
| Auto-reconnect | Built-in | Manual |
| Proxy-friendly | Yes | Sometimes blocked |
| Client API | EventSource | WebSocket |
2. Setting SSE Response Headers
Example: Required headers
res.set({
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" // tell Nginx not to buffer
});
res.flushHeaders();
3. Sending Event Data
4. Formatting SSE Messages
| Field | Purpose |
|---|---|
data: | Payload (multiple lines OK) |
event: | Custom event name |
id: | Last-Event-ID for reconnect |
retry: | Reconnect delay (ms) |
: comment | Heartbeat / no-op |
Example: Helper
function sse(res, { event, data, id, retry }) {
if (id) res.write(`id: ${id}\n`);
if (event) res.write(`event: ${event}\n`);
if (retry) res.write(`retry: ${retry}\n`);
res.write(`data: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`);
}
5. Implementing Event Types
Example: Multiple event names
sse(res, { event: "user.created", data: user });
sse(res, { event: "user.updated", data: user });
sse(res, { event: "user.deleted", data: { id } });
// Client: src.addEventListener("user.created", ...)
6. Handling Client Reconnection
Example: Resume from Last-Event-ID
app.get("/events", async (req, res) => {
// standard SSE headers...
const since = req.get("Last-Event-ID");
for await (const evt of bus.replay(since)) {
sse(res, { id: evt.id, event: evt.type, data: evt.payload });
}
// continue streaming new events...
});
7. Setting Retry Interval
8. Keeping Connection Alive
Example: Heartbeat comment
const ping = setInterval(() => res.write(": ping\n\n"), 15_000);
req.on("close", () => clearInterval(ping));
Warning: Most reverse proxies and load balancers idle out connections at 30–60s. Send a heartbeat comment every 10–20s.
9. Broadcasting Events to Clients
Example: Pub/sub fanout
const clients = new Set();
app.get("/feed", (req, res) => {
// standard SSE headers...
clients.add(res);
req.on("close", () => clients.delete(res));
});
export function broadcast(event, data) {
for (const res of clients) sse(res, { event, data });
}