What Are Server-Sent Events?
Server-Sent Events (SSE) is an HTTP-based mechanism for one-directional real-time streaming from server to client. A client opens a long-lived HTTP connection, and the server continuously pushes events down the wire.
SSE is part of the HTML5 specification (WHATWG) and works natively in every modern browser via the EventSource API — no library required.
SSE vs WebSocket
| SSE | WebSocket | |
|---|---|---|
| Direction | Server → Client only | Full duplex |
| Protocol | HTTP/1.1 or HTTP/2 | Upgraded TCP |
| Reconnection | Automatic | Manual |
| Proxy/firewall | Easy (plain HTTP) | Sometimes blocked |
| Browser API | `EventSource` | `WebSocket` |
| Use case | Notifications, feeds | Chat, games, collaboration |
Choose SSE when you need server-to-client push without bidirectional communication. For bidirectional real-time, use WebSockets.
The EventSource API
const source = new EventSource('/api/events');
// Default message event
source.onmessage = (event) => {
console.log('Received:', event.data);
};
// Named event type
source.addEventListener('order-update', (event) => {
const order = JSON.parse(event.data);
updateOrderUI(order);
})
source.onerror = (event) => {
if (source.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};
Server Implementation
Python (Django/FastAPI):
from django.http import StreamingHttpResponse
import json, time
def event_stream(request):
def generate():
while True:
data = get_latest_data() # your data source
yield f'event: update\n'
yield f'data: {json.dumps(data)}\n'
yield f'id: {int(time.time())}\n'
yield '\n' # blank line = end of event
time.sleep(1)
return StreamingHttpResponse(
generate(),
content_type='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no', # disable Nginx buffering
},
)
Node.js (Express):
app.get('/api/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify(getUpdate())}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
});
Event Types and IDs
The SSE format supports four fields per event:
event: order-update\n ← named event type (optional)
data: {"id": 42, ...}\n ← event payload
id: 1710000042\n ← event ID for reconnection
retry: 5000\n ← reconnection delay in ms
\n ← blank line terminates the event
Multi-line data: use multiple data: lines; the client joins them with newlines.
Automatic Reconnection
If the connection drops, the browser's EventSource automatically reconnects after 3 seconds (default) or the value from the last retry: field.
On reconnect, the browser sends a Last-Event-ID header with the last received event ID. Your server should use this to resume from where the client left off:
def event_stream(request):
last_id = request.headers.get('Last-Event-ID')
events = get_events_since(last_id) # replay missed events
...
Scalability Considerations
Each SSE connection holds an open HTTP connection. With 10,000 concurrent users, that is 10,000 open file descriptors.
- Use async workers (asyncio, Node.js) rather than synchronous workers — one thread per connection does not scale
- With Nginx, set
X-Accel-Buffering: noto prevent Nginx from buffering the stream - Over HTTP/2, multiple SSE streams share a single connection — better utilization than HTTP/1.1
- For very high connection counts, use a dedicated message broker (Redis Pub/Sub, Kafka) between your app and the SSE endpoint