Introduction
In the realm of real-time web communication, WebSockets often dominate the conversation. However, Server-Sent Events (SSE) offer a compelling alternative for scenarios where unidirectional server-to-client communication suffices. Established as a W3C standard and supported by all modern browsers, SSE provides a simpler, more reliable solution for many real-time use cases.
In 2026, SSE has gained renewed attention as developers recognize its advantages: automatic reconnection, simplicity of implementation, HTTP/2 compatibility, and seamless operation through firewalls and proxies. This guide explores Server-Sent Events in depth, from basic concepts to advanced patterns and production considerations.
Understanding Server-Sent Events
What Are Server-Sent Events?
Server-Sent Events (SSE) is a technology enabling servers to push data to clients over a single, long-lived HTTP connection. Unlike WebSockets, SSE is unidirectionalโonly the server can send messages to the client. This simplicity makes SSE ideal for live updates, notifications, dashboards, and streaming data.
The SSE protocol uses the MIME type text/event-stream and follows a simple message format:
data: {"message": "hello"}
data: second message\n\n
When to Use SSE vs WebSockets
| Aspect | Server-Sent Events | WebSockets |
|---|---|---|
| Direction | Server โ Client | Bidirectional |
| Protocol | HTTP/1.1 or HTTP/2 | WebSocket (ws://) |
| Browser Support | Universal | Universal |
| Automatic Reconnection | Built-in | Manual implementation |
| Binary Data | Limited | Full support |
| Connection Limits | ~6 per domain | Independent |
| Firewall Compatibility | High | May require config |
| Simplicity | Higher | Lower |
Choose SSE when:
- You only need server-to-client communication
- You want simpler implementation
- Automatic reconnection is important
- You need HTTP/2 multiplexing
- You want better firewall/proxy compatibility
Choose WebSockets when:
- You need bidirectional communication
- You need to send data back to the server frequently
- You need binary data transfer
- You require sub-second latency
The SSE Protocol
Message Format
SSE messages consist of fields, each on a new line:
field: value\n
Supported fields:
data: The message payload (required)id: Event identifier for automatic reconnectionevent: Custom event typeretry: Reconnection time in millisecondscomment: Ignored (for keep-alive)
Simple Example
Server response:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"temperature": 72}
data: {"temperature": 73}
data: {"temperature": 71}
Event IDs for Reliable Delivery
id: 1
data: {"type": "update", "content": "first message"}
id: 2
data: {"type": "update", "content": "second message"}
id: 3
data: {"type": "update", "content": "third message"}
When the connection drops, the browser automatically reconnects and sends the Last-Event-ID header, allowing the server to resume from where it left off.
Custom Event Types
event: notification
data: {"type": "alert", "message": "New message received"}
event: update
data: {"type": "data", "content": "database updated"}
event: heartbeat
data: ping
The client can listen for specific event types:
const source = new EventSource('/stream');
// Listen for specific events
source.addEventListener('notification', (e) => {
console.log('Notification:', JSON.parse(e.data));
});
source.addEventListener('update', (e) => {
console.log('Update:', JSON.parse(e.data));
});
Implementing SSE on the Server
Node.js Implementation
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/stream' && req.accepts('text/event-stream')) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// Send initial message
res.write('data: {"status": "connected"}\n\n');
// Send updates every 2 seconds
const interval = setInterval(() => {
const data = {
timestamp: new Date().toISOString(),
value: Math.random() * 100
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 2000);
// Handle client disconnect
req.on('close', () => {
clearInterval(interval);
console.log('Client disconnected');
});
// Send comment to keep connection alive
const keepAlive = setInterval(() => {
res.write(': keepalive\n\n');
}, 30000);
req.on('close', () => clearInterval(keepAlive));
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('SSE server running on http://localhost:3000/stream');
});
Python Implementation
from flask import Flask, Response, stream_with_context
import time
import json
app = Flask(__name__)
@app.route('/stream')
def stream():
def generate():
# Send initial message
yield 'data: {"status": "connected"}\n\n'
# Stream updates
for i in range(100):
data = {
'timestamp': time.time(),
'counter': i
}
yield f'data: {json.dumps(data)}\n\n'
time.sleep(1)
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
}
)
if __name__ == '__main__':
app.run(threaded=True)
Go Implementation
package main
import (
"fmt"
"net/http"
"time"
)
func sseHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// Flush headers
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", 500)
return
}
// Send initial message
fmt.Fprintf(w, "data: {\"status\": \"connected\"}\n\n")
flusher.Flush()
// Create ticker for periodic updates
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
data := map[string]interface{}{
"timestamp": time.Now().Unix(),
"value": time.Now().Unix() % 100,
}
fmt.Fprintf(w, "data: %v\n\n", data)
flusher.Flush()
case <-r.Context().Done():
return
}
}
}
func main() {
http.HandleFunc("/stream", sseHandler)
http.ListenAndServe(":8080", nil)
}
Using Resumable Events
from flask import Flask, request, Response
import time
app = Flask(__name__)
# In-memory storage (use database in production)
events = []
@app.route('/stream')
def stream():
last_id = request.headers.get('Last-Event-ID', '0')
# Resume from last event
try:
resume_index = int(last_id)
except ValueError:
resume_index = 0
def generate():
# Send missed events
for event in events[resume_index:]:
yield f"id: {event['id']}\ndata: {event['data']}\n\n"
# Stream new events
while True:
if len(events) > resume_index:
event = events[-1]
yield f"id: {event['id']}\ndata: {event['data']}\n\n"
resume_index = event['id']
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
# Endpoint to generate events
@app.route('/send', methods=['POST'])
def send_event():
event_id = len(events)
# ... handle POST request ...
events.append({
'id': event_id,
'data': '{"message": "new event"}'
})
return 'OK'
Implementing SSE on the Client
Basic Usage
const source = new EventSource('/stream');
source.onopen = () => {
console.log('Connection opened');
};
source.onmessage = (event) => {
console.log('Received:', event.data);
};
source.onerror = (error) => {
console.error('SSE Error:', error);
};
Handling Reconnection
const source = new EventSource('/stream');
// Track last event ID for resumption
let lastEventId = null;
source.addEventListener('message', (event) => {
lastEventId = event.lastEventId;
console.log('Data:', event.data);
});
// When reconnecting, Last-Event-ID is automatically sent
source.addEventListener('open', () => {
console.log('Reconnected, last ID:', lastEventId);
});
Custom Event Handling
const source = new EventSource('/api/updates');
// Real-time notifications
source.addEventListener('notification', (e) => {
const data = JSON.parse(e.data);
showNotification(data.title, data.body);
});
// Dashboard updates
source.addEventListener('dashboard', (e) => {
const metrics = JSON.parse(e.data);
updateDashboard(metrics);
});
// Price updates
source.addEventListener('price', (e) => {
const price = JSON.parse(e.data);
updatePriceDisplay(price);
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
source.close();
});
Manual Reconnection Control
class SSEManager {
constructor(url) {
this.url = url;
this.source = null;
this.retryTime = 5000;
this.maxRetries = 5;
}
connect() {
this.source = new EventSource(this.url);
this.source.onopen = () => {
console.log('SSE connected');
this.retryTime = 5000; // Reset on success
};
this.source.onerror = () => {
console.log('SSE error, retrying in', this.retryTime);
this.source.close();
if (this.retryTime < this.maxRetries * 1000) {
setTimeout(() => this.connect(), this.retryTime);
this.retryTime *= 2;
}
};
return this.source;
}
on(event, handler) {
if (this.source) {
this.source.addEventListener(event, handler);
}
}
close() {
if (this.source) {
this.source.close();
}
}
}
// Usage
const sse = new SSEManager('/api/stream');
sse.connect();
sse.on('update', (e) => console.log(e.data));
SSE with HTTP/2
Benefits of HTTP/2
HTTP/2 provides significant advantages for SSE:
- Multiplexing: Multiple streams over single connection
- Header Compression: Reduced overhead
- Server Push: Additional capabilities alongside SSE
- Connection Reuse: Single connection for multiple resources
Node.js with HTTP/2
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt')
});
server.on('stream', (stream, headers) => {
if (headers[':path'] === '/stream') {
stream.respond({
':status': 200,
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Send initial data
stream.write('data: {"status": "connected"}\n\n');
const interval = setInterval(() => {
const data = { time: Date.now() };
stream.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
stream.on('close', () => clearInterval(interval));
} else {
stream.respond({ ':status': 404 });
stream.end();
}
});
server.listen(8443, () => {
console.log('HTTP/2 SSE server running on https://localhost:8443');
});
Production Considerations
Scaling with Message Queues
import redis
import json
r = redis.Redis()
def sse_handler(environ, start_response):
channel = environ['PATH_INFO'].strip('/')
def generate():
pubsub = r.pubsub()
pubsub.subscribe(channel)
for message in pubsub.listen():
if message['type'] == 'message':
yield f"data: {message['data']}\n\n"
return generate()
# Publishing events
def publish_event(channel, data):
r.publish(channel, json.dumps(data))
Connection Limits
Browser limits:
- Chrome: 6 connections per origin (HTTP/1.1)
- Firefox: 6 connections per origin
- HTTP/2: 100+ multiplexed streams
Solutions:
- Use HTTP/2
- Implement connection pooling
- Distribute across subdomains
- Use CDN for SSE endpoints
Authentication
// Client: Include token in URL
const source = new EventSource('/stream?token=YOUR_JWT_TOKEN');
// Server: Validate token
app.get('/stream', (req, res) => {
const token = req.query.token;
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
} catch (e) {
res.status(401).end();
return;
}
// Stream events
});
Heartbeats and Keep-Alive
// Server: Send comment every 30 seconds
setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
// Client: Detect connection issues
const source = new EventSource('/stream');
let lastMessageTime = Date.now();
source.onmessage = () => {
lastMessageTime = Date.now();
};
// Check for stale connections
setInterval(() => {
if (Date.now() - lastMessageTime > 60000) {
console.log('Connection may be stale');
}
}, 10000);
Real-World Use Cases
Live Dashboards
// Client: Dashboard metrics
const source = new EventSource('/api/metrics/realtime');
source.addEventListener('cpu', (e) => {
updateCpuGauge(JSON.parse(e.data));
});
source.addEventListener('memory', (e) => {
updateMemoryChart(JSON.parse(e.data));
});
source.addEventListener('network', (e) => {
updateNetworkGraph(JSON.parse(e.data));
});
Notification
// Client: User notifications const source = new EventSource Systems
source.addEventListener('new_order', (e) => {
showToast('New order received!', 'success');
});
source.addEventListener('system_alert', (e) => {
showAlert(JSON.parse(e.data));
});
source.addEventListener('message', (e) => {
incrementUnreadCount();
showChatNotification(JSON.parse(e.data));
});
Live Feeds
// Client: Social media feed
const source = new EventSource('/api/feed/live');
source.addEventListener('post', (e) => {
const post = JSON.parse(e.data);
prependToFeed(post);
});
source.addEventListener('like', (e) => {
updateLikeCount(e.data);
});
source.addEventListener('comment', (e) => {
appendComment(e.data);
});
Best Practices
- Use HTTP/2 When Possible: Better multiplexing and efficiency
- Implement Proper Reconnection: Leverage event IDs for reliability
- Include Heartbeats: Prevent proxy timeouts
- Handle Connection Limits: Plan for browser limits
- Secure Your Streams: Use authentication and TLS
- Monitor Connections: Track active connections and failures
- Graceful Degradation: Have fallback mechanisms ready
- Test Across Browsers: Verify consistent behavior
Conclusion
Server-Sent Events provide a robust, simple solution for server-to-client real-time communication. While not suitable for every use case, SSE excels in scenarios involving live updates, notifications, dashboards, and streaming data where unidirectional communication suffices.
By understanding the SSE protocol, implementing proper error handling, and following production best practices, you can build reliable real-time applications that scale effectively. The automatic reconnection, HTTP compatibility, and simplicity of SSE make it an excellent choice for modern web applications.
Resources
- MDN: Server-Sent Events
- W3C Server-Sent Events Specification
- Can I Use: EventSource
- Flask-SSE: SSE for Python
- Server-Sent Events vs WebSockets
Comments