Server-Sent Events (SSE)

intermediate sse real-time eventsource streaming http

Server-Sent Events (SSE) is a one-way streaming channel from server to client over plain HTTP. The server sends events whenever it wants; the client just listens.

In simple language: SSE is a pipe the server keeps writing to. The browser keeps reading. No frames, no upgrade dance — just HTTP that doesn’t end.

How It Works

The client makes a normal GET request. The server replies with Content-Type: text/event-stream and never closes the connection. It sends data in a simple text format:

data: hello

data: how are you?

event: chat
data: {"user":"manish","text":"hi"}

Each event ends with a blank line. That’s the entire wire format.

When SSE Fits

  • Server pushes updates to clients (notifications, stock tickers, log tails).
  • We don’t need the client to send messages back through the same channel.
  • We want auto-reconnect for free.
  • We want to get through corporate proxies that block WebSockets.

If we need bidirectional communication, use WebSockets instead.

Server Example (Node)

import express from "express";

const app = express();

app.get("/events", (req, res) => {
  // Required SSE headers
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.flushHeaders(); // send headers immediately

  // Send an event every 2 seconds
  const interval = setInterval(() => {
    const payload = { time: new Date().toISOString() };
    res.write(`event: tick\n`);
    res.write(`data: ${JSON.stringify(payload)}\n\n`);
  }, 2000);

  // Clean up when client disconnects
  req.on("close", () => clearInterval(interval));
});

app.listen(3000);

Notice each message ends with \n\n — that’s the event boundary.

Client Example (EventSource)

const events = new EventSource("/events");

// Default "message" channel
events.addEventListener("message", (e) => {
  console.log("msg:", e.data);
});

// Custom event names (we used "tick" on the server)
events.addEventListener("tick", (e) => {
  const data = JSON.parse(e.data);
  console.log("tick at", data.time);
});

// Browser auto-reconnects on disconnect
events.addEventListener("error", (err) => {
  console.warn("connection lost, browser will retry...");
});

// Manually close when we don't need it anymore
// events.close();

The browser’s EventSource API handles reconnection automatically. No backoff logic to write ourselves.

Reconnection & Last-Event-ID

SSE has a built-in resume mechanism. The server can send an id: field with each event:

id: 42
data: {"text":"hello"}

If the connection drops, the browser reconnects and sends Last-Event-ID: 42 header. The server can then resume from where we left off.

SSE vs WebSockets vs Long Polling

FeatureSSEWebSocketsLong Polling
DirectionServer → clientBoth waysBoth ways
ProtocolHTTPws/wssHTTP
Auto-reconnectYes (built-in)NoPer-request
BinaryNo (text only)YesYes
Proxy-friendlyVeryMostlyYes
Setup complexityTrivialModerateModerate

Common Gotchas

  • Connection limits — browsers cap concurrent connections per origin (often 6 over HTTP/1.1). On HTTP/2 this isn’t a problem.
  • No binary — SSE is text-only. For binary, base64-encode it or use WebSockets.
  • Buffering — make sure the response isn’t being buffered by a reverse proxy. With nginx, set X-Accel-Buffering: no or proxy_buffering off.
  • Timeout — some proxies drop idle connections after 60s. Send a keepalive comment (: ping\n\n) periodically.

Interview Tip

Best one-liner: “SSE is a long-lived HTTP response with Content-Type: text/event-stream.” If we say that and mention auto-reconnect via Last-Event-ID, we’ve covered the essentials.