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
| Feature | SSE | WebSockets | Long Polling |
|---|---|---|---|
| Direction | Server → client | Both ways | Both ways |
| Protocol | HTTP | ws/wss | HTTP |
| Auto-reconnect | Yes (built-in) | No | Per-request |
| Binary | No (text only) | Yes | Yes |
| Proxy-friendly | Very | Mostly | Yes |
| Setup complexity | Trivial | Moderate | Moderate |
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: noorproxy_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.