SSE — Server-Sent Events
SSE is a one-way, server-to-client push mechanism over a regular HTTP connection. The server streams events to the browser indefinitely. Simpler than WebSockets when you only need server → client updates.
SSE vs WebSocket — The Core Difference
WebSocket:
Client ←──────────────────▶ Server
(full-duplex, both send)
SSE:
Client ◀──────────────────── Server
(server pushes only)
If your client doesn't need to send data to the server in real time (only the server pushes), SSE is simpler and uses plain HTTP — no upgrade handshake, automatic reconnect built in.
How SSE Works
Client sends a regular HTTP GET:
────────────────────────────────
GET /events HTTP/1.1
Accept: text/event-stream
Cache-Control: no-cache
Server responds with:
────────────────────────────────
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"type":"notification","text":"Hello!"}
data: {"type":"notification","text":"Price updated"}
event: user-login
data: {"userId":"42","name":"Madhu"}
id: 100
data: {"type":"message","text":"Hey"}
: this is a comment, ignored by browser
retry: 3000
The connection stays open forever. Server sends events as they happen. Each event ends with
\n\n(two newlines).
SSE Event Format
# Basic event (just data)
data: Hello World\n\n
# Multi-line data
data: line 1\n
data: line 2\n
data: line 3\n\n
# Named event (triggers specific event listener)
event: price-update\n
data: {"symbol":"AAPL","price":182.50}\n\n
# Event with ID (for reconnect resume)
id: 42\n
data: {"message":"Checkpoint 42"}\n\n
# Custom retry interval (milliseconds)
retry: 5000\n\n
# Comment (keepalive, ignored by browser)
: keepalive\n\n
Browser EventSource API
// Connect to SSE endpoint
const source = new EventSource('/api/events');
// With credentials (cookies)
const source = new EventSource('/api/events', { withCredentials: true });
// Default message event (no "event:" field)
source.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Message:', data);
console.log('ID:', event.lastEventId);
});
// Named event listener
source.addEventListener('price-update', (event) => {
const { symbol, price } = JSON.parse(event.data);
updatePriceDisplay(symbol, price);
});
source.addEventListener('user-login', (event) => {
const { userId } = JSON.parse(event.data);
console.log(`User ${userId} logged in`);
});
// Connection opened
source.addEventListener('open', () => {
console.log('SSE connected');
});
// Error + auto-reconnect
source.addEventListener('error', (event) => {
if (source.readyState === EventSource.CLOSED) {
console.log('Connection closed permanently');
} else {
// Browser will auto-reconnect (this is built in!)
console.log('Connection error, will retry...');
}
});
// Close connection
source.close();
Automatic Reconnection (SSE's Killer Feature)
SSE reconnects automatically — no code needed.
Flow:
1. Connection drops
2. Browser waits retry ms (default 3000ms)
3. Browser sends: Last-Event-ID: 42 (last seen event ID)
4. Server resumes from where it left off
Server-side: honor Last-Event-ID header
────────────────────────────────────────
GET /events
Last-Event-ID: 42
Server: "OK, sending events from ID > 42"
# In Node.js:
app.get('/events', (req, res) => {
const lastId = parseInt(req.headers['last-event-id']) || 0;
// Send only events after lastId
});
Server Implementation Examples
Node.js (Express)
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering
// Send initial event
res.write('data: {"type":"connected"}\n\n');
// Send events every 5 seconds
const interval = setInterval(() => {
const event = {
id: Date.now(),
data: { timestamp: new Date().toISOString(), value: Math.random() }
};
res.write(`id: ${event.id}\n`);
res.write(`data: ${JSON.stringify(event.data)}\n\n`);
}, 5000);
// Send keepalive comment every 30 seconds (prevents proxy timeouts)
const keepalive = setInterval(() => {
res.write(': keepalive\n\n');
}, 30000);
// Cleanup on disconnect
req.on('close', () => {
clearInterval(interval);
clearInterval(keepalive);
});
});
Python (FastAPI)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
app = FastAPI()
async def event_generator(request):
while True:
if await request.is_disconnected():
break
yield f"data: {json.dumps({'time': time.time()})}\n\n"
await asyncio.sleep(5)
@app.get("/events")
async def sse_endpoint(request: Request):
return StreamingResponse(
event_generator(request),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
SSE Mindmap
mindmap
root((Server-Sent Events))
Protocol
Regular HTTP GET
Content-Type text/event-stream
Persistent connection keep-alive
Always HTTP 200
Event Format
data field required
event field optional named events
id field for reconnect resume
retry field reconnect delay
Comments colon prefix keepalive
Browser API
EventSource constructor
message event default
Named event listeners
open event
error event
readyState
close method
Auto-Reconnect
Built into browser
Sends Last-Event-ID header
retry ms configurable
Server resumes from last ID
Use Cases
Live notifications
Dashboard metrics
Build/deploy logs
Social media feeds
AI streaming responses
Stock price tickers
vs WebSocket
Server-only direction
HTTP no upgrade
Auto-reconnect built-in
Simpler to implement
No binary support
HTTP/2 compatible
SSE for AI Streaming (ChatGPT-style)
SSE is exactly how AI chat interfaces stream token-by-token responses.
// Client
const source = new EventSource('/api/generate');
let fullText = '';
source.addEventListener('token', (event) => {
fullText += event.data;
updateUI(fullText); // Update as tokens arrive
});
source.addEventListener('done', () => {
source.close();
});
// Server (Node.js)
app.get('/api/generate', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
const stream = await llm.stream("Tell me about APIs...");
for await (const token of stream) {
res.write(`event: token\ndata: ${token}\n\n`);
}
res.write('event: done\ndata: {}\n\n');
res.end();
});
SSE Limitations & Workarounds
| Limitation | Details | Workaround |
|---|---|---|
| Server → client only | Can't send from client | Use separate HTTP POST for client→server |
| Max 6 connections per origin (HTTP/1.1) | Browser limit per domain | Use HTTP/2 (multiplexed, no limit) |
| No binary data | Text only | Base64 encode binary data |
| IE/Edge old versions | No native support | eventsource polyfill |
| Proxy timeouts | Some proxies close idle connections | Send ': keepalive\n\n' every 30s |
When to Use SSE vs WebSocket
Use SSE when:
✅ Server pushes updates to client only
✅ Notifications, alerts, live feeds
✅ AI response streaming
✅ Dashboard metrics / monitoring
✅ Build logs, deployment progress
✅ Want simpler server implementation
✅ HTTP/2 is available (efficient multiplexing)
Use WebSocket when:
✅ Client also needs to send real-time data
✅ Chat, collaborative editing, multiplayer
✅ Low-latency bidirectional is critical
✅ Binary data (audio, video frames)
Interview tips: - SSE = HTTP-based, server-push only, auto-reconnect built in - WebSocket = TCP upgrade, full-duplex, manual reconnect - SSE is how ChatGPT/Claude stream responses (token by token) -
Last-Event-IDheader enables resume-from-last-event after disconnect - HTTP/2 removes the 6-connection limit — SSE becomes more scalable