Implementing Server-Sent Events

1. Understanding SSE Protocol

AspectSSEWebSocket
DirectionServer → Client onlyBidirectional
TransportHTTP/1.1+ chunkedUpgrade to ws://
Auto-reconnectBuilt-inManual
Proxy-friendlyYesSometimes blocked
Client APIEventSourceWebSocket

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

Example: write line

res.write(`data: ${JSON.stringify({ msg: "hello" })}\n\n`);

4. Formatting SSE Messages

FieldPurpose
data:Payload (multiple lines OK)
event:Custom event name
id:Last-Event-ID for reconnect
retry:Reconnect delay (ms)
: commentHeartbeat / 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

Example: Tell client to retry in 10s

res.write("retry: 10000\n\n");

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 });
}

10. Closing SSE Connections

Example: Server-initiated close

sse(res, { event: "shutdown", data: { reason: "deploy" } });
res.end();  // browser will reconnect automatically per retry: directive