A one-way real-time communication protocol from server to client, over a single persistent HTTP connection.
The client makes a single HTTP GET request. Instead of the server sending one response and closing the connection, it keeps the connection open and pushes plain-text events down the wire whenever it has something new to say.
That's the entire trick. No special protocol negotiation, no binary framing, no upgrade handshake. Just an HTTP response that never ends.
Each event is a block of field: value lines, terminated by a blank line. There are only four possible fields:
data:data: lines are joined with newlines.event:id:retry:The browser's EventSource API handles connection management, parsing, and reconnection. The entire client is a few lines:
const source = new EventSource('/events');
// Default "message" events
source.onmessage = (e) => {
console.log(e.data); // the payload string
console.log(e.lastEventId); // the id: field
};
// Named events (event: notification)
source.addEventListener('notification', (e) => {
console.log('Custom:', e.data);
});
// Connection lifecycle
source.onopen = () => console.log('Connected');
source.onerror = () => console.log('Reconnecting...');
// Explicitly close (no auto-reconnect)
source.close();
EventSource automatically reconnects on network failures. The browser sends a Last-Event-ID header with the last id: value it received, so the server can resume from where it left off.
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let id = 0;
const interval = setInterval(() => {
id++;
res.write(`id: ${id}\n`);
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 2000);
// Clean up on disconnect
req.on('close', () => clearInterval(interval));
}
}).listen(3000);
Cache-Control: no-cache to prevent proxies from buffering the stream. If you're behind Nginx, you'll also need X-Accel-Buffering: no.
Open a simulated SSE connection and watch the protocol in action. Send events, trigger disconnects, and observe auto-reconnection.
| Feature | SSE | WebSockets | Polling |
|---|---|---|---|
| Direction | Server → Client | Bidirectional | Client → Server |
| Protocol | HTTP | WS (upgrade) | HTTP |
| Auto-reconnect | Built in | Manual | N/A |
| Event IDs | Built in | Manual | Manual |
| Binary data | No (text only) | Yes | Yes |
| Complexity | Very low | Medium | Low |
| Best for | Live feeds, notifications, streaming AI | Chat, gaming, collaboration | Infrequent updates |
Client sends a GET with Accept: text/event-stream. Server responds 200 OK with Content-Type: text/event-stream and keeps the connection open.
Server writes events as plain text, each terminated by a blank line. The browser parses each chunk and fires the appropriate event handler. Keepalive comments (: ping) prevent proxy timeouts.
If the connection drops, the browser waits (default ~3s, configurable via retry:) and automatically re-opens it. The Last-Event-ID header lets the server resume from the right point.
Calling source.close() terminates the connection permanently — no auto-reconnect. The server sees the TCP connection close and should clean up resources.
X-Accel-Buffering: no for Nginx, or use chunked transfer encoding.
data: lines — the browser joins them with \n. Or just JSON-encode your payload in a single data: line.