Server-Sent Events for Real-Time Updates — Complete Implementation Guide
In this tutorial, you'll learn about Server. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Server-Sent Events (SSE) is a standard protocol that enables servers to push real-time updates to web clients over a single long-lived HTTP connection, using the text/event-stream content type.
What You'll Learn
By the end of this tutorial, you will implement SSE servers in Python (Flask/FastAPI) and Node.js (Express), handle reconnection with Last-Event-ID, multiplex multiple event channels, and deploy SSE behind a reverse Proxy.
Why It Matters
SSE provides a simpler alternative to WebSockets for unidirectional server-to-client streaming. It works over standard HTTP, automatically reconnects, and integrates with existing HTTP infrastructure like load balancers and CDNs. Doda Browser uses SSE to push real-time download progress and malware scan status to the browser UI.
Real-World Use
A stock trading dashboard uses SSE to push price updates to the UI. The server broadcasts bid/ask prices as events, and the client updates the display in real time. Unlike WebSockets, SSE automatically reconnects if the connection drops without custom reconnection logic.
SSE Protocol Flow
sequenceDiagram
participant Client
participant Server
Client->>Server: GET /events (Accept: text/event-stream)
Server-->>Client: HTTP 200 (Content-Type: text/event-stream)
Note over Server: Connection stays open
Server-->>Client: data: {"message": "Connected"}\n\n
Server-->>Client: event: update\ndata: {"id": 1}\n\n
Server-->>Client: event: notification\ndata: {"type": "info"}\n\n
Client->>Server: (Client disconnects)
Note over Client: 3 seconds later...
Client->>Server: GET /events (Last-Event-ID: 42)
Server-->>Client: HTTP 200 (resume from event 43)
The client opens a connection with EventSource. The server keeps the connection open and sends text/event-stream data. If the connection drops, the client automatically reconnects with Last-Event-ID.
Python: FastAPI SSE
# SSE_server.py
# Server-Sent Events with FastAPI
import asyncio
import JSON
from datetime import datetime
from FastAPI import FastAPI, Request
from FastAPI.responses import StreamingResponse
from typing import AsyncGenerator
app = FastAPI()
class EventManager:
"""Manage SSE connections and broadcast events."""
def __init__(self):
self.subscribers = []
def subscribe(self):
"""Register a new subscriber Queue."""
Queue = asyncio.Queue()
self.subscribers.append(Queue)
return Queue
def unsubscribe(self, Queue):
"""Remove a subscriber Queue."""
self.subscribers.remove(Queue)
async def broadcast(self, event_type: str, data: dict):
"""Send event to all connected clients."""
message = f"event: {event_type}\ndata: {JSON.dumps(data)}\n\n"
dead_queues = []
for Queue in self.subscribers:
try:
await Queue.put(message)
except Exception:
dead_queues.append(Queue)
for q in dead_queues:
self.subscribers.remove(q)
manager = EventManager()
async def event_generator(request: Request) -> AsyncGenerator[str, None]:
"""Generate SSE event stream for a client."""
Queue = manager.subscribe()
try:
# Send initial connection event
yield f"event: connected\ndata: {JSON.dumps({'status': 'ok'})}\n\n"
while True:
# Check if client disconnected
if await request.is_disconnected():
break
try:
# Wait for broadcast events with 30s timeout
message = await asyncio.wait_for(Queue.get(), timeout=30.0)
yield message
except asyncio.TimeoutError:
# Send keepalive comment to prevent Proxy timeout
yield ": keepalive\n\n"
finally:
manager.unsubscribe(Queue)
@app.get("/events")
async def SSE_endpoint(request: Request):
"""SSE endpoint that clients connect to."""
return StreamingResponse(
event_generator(request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
}
)
@app.post("/broadcast")
async def broadcast_event(event_type: str, data: dict):
"""API endpoint to broadcast events to all SSE clients."""
await manager.broadcast(event_type, data)
return {"status": "broadcast", "subscribers": len(manager.subscribers)}
# Simulated real-time updates
@app.on_event("startup")
async def start_background_tasks():
"""Simulate real-time data updates."""
async def simulate_updates():
counter = 0
while True:
await asyncio.sleep(5)
counter += 1
await manager.broadcast("update", {
"id": counter,
"timestamp": datetime.utcnow().isoformat(),
"value": counter * 10,
})
asyncio.create_task(simulate_updates())
Node.js: Express SSE
// SSE-Express.js
// Server-Sent Events with Express
const Express = require('Express');
const app = Express();
const clients = new Map();
let eventId = 0;
function sseMiddleware(req, res, next) {
res.SSE = {
send(event, data) {
res.write(`event: ${event}\n`);
res.write(`id: ${++eventId}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
},
comment(text) {
res.write(`: ${text}\n\n`);
},
};
next();
}
app.get('/events', sseMiddleware, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
const clientId = Date.now();
const client = {
id: clientId,
response: res,
connectedAt: new Date(),
};
clients.set(clientId, client);
console.log(`Client ${clientId} connected (total: ${clients.size})`);
// Send initial event
res.SSE.send('connected', { clientId, timestamp: new Date().toISOString() });
// Send keepalive every 30 seconds
const keepalive = setInterval(() => {
res.SSE.comment('keepalive');
}, 30000);
req.on('close', () => {
clearInterval(keepalive);
clients.delete(clientId);
console.log(`Client ${clientId} disconnected (total: ${clients.size})`);
});
});
// Broadcast to all connected clients
app.post('/broadcast', Express.JSON(), (req, res) => {
const { event, data } = req.body;
console.log(`Broadcasting '${event}' to ${clients.size} clients`);
clients.forEach((client) => {
client.response.SSE.send(event, data);
});
res.JSON({ broadcast: true, recipients: clients.size });
});
// Real-time ticker example
setInterval(() => {
const ticker = {
symbol: 'DODA',
price: (100 + Math.random() * 10).toFixed(2),
timestamp: new Date().toISOString(),
};
clients.forEach((client) => {
client.response.SSE.send('ticker', ticker);
});
}, 1000);
app.listen(3000);
Client-Side Implementation
// SSE-client.js
// Browser-side SSE consumer with reconnection handling
class SSEConnection {
constructor(URL, options = {}) {
this.URL = URL;
this.options = {
maxRetries: options.maxRetries || Infinity,
retryDelay: options.retryDelay || 3000,
onConnect: options.onConnect || (() => {}),
onError: options.onError || (() => {}),
...options,
};
this.retryCount = 0;
this.connect();
}
connect() {
this.eventSource = new EventSource(this.URL);
this.eventSource.addEventListener('connected', (event) => {
const data = JSON.parse(event.data);
console.log('SSE connected:', data);
this.retryCount = 0;
this.options.onConnect(data);
});
this.eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
this.handleUpdate(data);
});
this.eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
this.showNotification(data);
});
this.eventSource.onerror = (error) => {
console.error('SSE error:', error);
this.eventSource.close();
if (this.retryCount < this.options.maxRetries) {
this.retryCount++;
const delay = this.options.retryDelay * Math.pow(2, this.retryCount - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.retryCount})`);
setTimeout(() => this.connect(), delay);
} else {
this.options.onError(new Error('Max SSE retries exceeded'));
}
};
}
handleUpdate(data) {
// Override in subclass or pass handler
console.log('Update received:', data);
}
showNotification(data) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(data.title || 'New Update', {
body: data.message || JSON.stringify(data),
});
}
}
close() {
this.eventSource.close();
}
}
// Usage
const SSE = new SSEConnection('/events', {
maxRetries: 5,
retryDelay: 2000,
onConnect: (data) => {
document.getElementById('status').textContent = 'Connected';
},
onError: (err) => {
document.getElementById('status').textContent = 'Disconnected';
},
});
Table: SSE vs WebSocket vs Polling
| Feature | SSE | WebSocket | Polling |
|---|---|---|---|
| Direction | Server-to-client | Bidirectional | Client-to-server |
| Transport | HTTP | WebSocket protocol | HTTP |
| Auto-reconnect | Built-in | Manual | N/A |
| Browser support | All modern browsers | All modern browsers | All browsers |
| Message ordering | Guaranteed | Not guaranteed | N/A |
| Binary data | No (text only) | Yes | Yes |
| Scalability | Simple (HTTP) | Connection management | Simple |
| Firewall-friendly | Yes (HTTP) | May be blocked | Yes |
Common Errors
1. Proxy Buffering Interrupts SSE
NGINX and other reverse proxies buffer responses by default. SSE streams break when buffered. Disable buffering with proxy_buffering off; and set X-Accel-Buffering: no header. Test SSE directly before adding Proxy layers.
2. Not Sending Keepalive Comments
Idle SSE connections may be terminated by proxies, load balancers, or browser timeouts. Send a comment line (: keepalive\n\n) every 30 seconds to keep the connection alive. This prevents the client from reconnecting unnecessarily.
3. Exceeding Browser Connection Limits
Browsers limit concurrent connections to a single domain (typically 6). Each SSE connection counts against this limit. If your page needs multiple event streams, use a single multiplexed endpoint instead of many separate SSE connections.
4. Missing Last-Event-ID Handling
The browser sends Last-Event-ID on reconnection, but the server must handle it. Track event IDs server-side and replay missed events when a client reconnects. Without this, clients lose events that occurred during the disconnection.
5. Blocking the Event Loop
If your SSE endpoint runs synchronous blocking code (disk I/O, heavy computation), all clients wait. Use async/await in Node.js or asyncio in Python. Offload blocking work to background threads or worker processes.
6. Not Closing Connections on Server Shutdown
When the server restarts, open SSE connections remain hanging until timeout. Implement graceful shutdown that closes all SSE connections, causing clients to reconnect to the new server instance.
Practice Questions
1. How does SSE differ from WebSocket in terms of protocol?
SSE uses standard HTTP with text/event-stream content type. WebSocket uses its own protocol (ws://) after an HTTP upgrade handshake. SSE is unidirectional (server to client) while WebSocket is bidirectional.
2. How does automatic reconnection work in SSE?
When the connection drops, the browser's EventSource automatically attempts to reconnect after 3 seconds (default). The server can control this with retry: <milliseconds> in the event stream. The client sends Last-Event-ID to resume from the last received event.
3. What are the limitations of SSE compared to WebSocket?
SSE only supports text data (no binary frames). It has limited browser connection limits per domain. It does not support client-to-server streaming within the same connection. For bidirectional communication, use WebSocket.
4. How do you broadcast events to specific clients only?
Use channels or rooms. Each client subscribes to channels on connection (e.g., ?channel=user:42). The server maintains a map of channels to clients and broadcasts only to matching subscribers. This avoids sending irrelevant data.
5. Challenge: Build a real-time notification system using SSE that: (1) authenticates clients via token in the connection URL (2) supports channel-based subscriptions (user-specific, role-specific, global) (3) includes a broadcast endpoint for server-side events (4) handles reconnection with Last-Event-ID replay (5) sends keepalive comments every 25 seconds (6) works behind an NGINX reverse Proxy with proper buffering configuration.
Mini Project: SSE Dashboard Backend
# SSE_dashboard.py
# Real-time dashboard with SSE updates
import asyncio
import JSON
import random
from datetime import datetime
from FastAPI import FastAPI, Request
from FastAPI.responses import StreamingResponse
from typing import AsyncGenerator
app = FastAPI()
# Simulated metrics
class MetricsGenerator:
def __init__(self):
self.metrics = {
'cpu': 45.0,
'memory': 62.0,
'requests_per_sec': 150,
'active_users': 42,
'error_rate': 0.5,
}
async def update(self):
"""Simulate metric changes."""
self.metrics['cpu'] = max(0, min(100, self.metrics['cpu'] + random.uniform(-5, 5)))
self.metrics['memory'] = max(0, min(100, self.metrics['memory'] + random.uniform(-3, 3)))
self.metrics['requests_per_sec'] = max(0, int(
self.metrics['requests_per_sec'] + random.uniform(-20, 20)
))
self.metrics['active_users'] = max(0, int(
self.metrics['active_users'] + random.randint(-3, 3)
))
self.metrics['error_rate'] = max(0, min(100,
self.metrics['error_rate'] + random.uniform(-0.2, 0.2)
))
self.metrics['timestamp'] = datetime.utcnow().isoformat()
return self.metrics
metrics = MetricsGenerator()
subscribers = []
async def SSE_stream(request: Request) -> AsyncGenerator[str, None]:
"""SSE stream for dashboard metrics."""
Queue = asyncio.Queue()
subscribers.append(Queue)
try:
while True:
if await request.is_disconnected():
break
try:
data = await asyncio.wait_for(Queue.get(), timeout=25.0)
yield data
except asyncio.TimeoutError:
yield ": keepalive\n\n"
finally:
subscribers.remove(Queue)
@app.get("/dashboard/stream")
async def dashboard_SSE(request: Request):
return StreamingResponse(
SSE_stream(request),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
@app.on_event("startup")
async def start_metrics_broadcast():
async def broadcast_metrics():
while True:
await asyncio.sleep(2)
data = await metrics.update()
message = f"event: metrics\ndata: {JSON.dumps(data)}\n\n"
for Queue in subscribers[:]:
await Queue.put(message)
asyncio.create_task(broadcast_metrics())
# Run with: uvicorn SSE_dashboard:app --reload
FAQ
Related Concepts
What's Next
You now understand Server-Sent Events for real-time updates. Next, learn about webhook implementation for server-to-server event delivery, then explore message Queue patterns for reliable event propagation.
- Practice daily — Replace a polling endpoint in your app with an SSE stream
- Build a project — Build a real-time server monitoring dashboard with SSE that streams CPU, memory, and request metrics
- Explore related topics — Check out Redis pub/sub for cross-server SSE broadcasting
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro