Server-Sent Events (SSE) provide a powerful way to implement real-time features in web applications. Unlike WebSocket, SSE is perfect for scenarios where you need one-way communication from server to client, such as live updates, notifications, or data streams. Let's explore how to implement SSE effectively.
Understanding Server-Sent Events
SSE establishes a persistent connection between the server and client, allowing the server to push data to the client whenever needed. Key advantages include:
- Automatic reconnection
- Built-in event IDs
- Native browser support
- Simpler than WebSocket for one-way communication
- Works over regular HTTP
Server Implementation
Node.js/Express Server
Create an Express server that supports SSE:
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
// Set SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Send initial connection message
res.write('data: Connected to event stream\n\n');
// Keep connection alive
const intervalId = setInterval(() => {
res.write(`data: ${new Date().toISOString()}\n\n`);
}, 30000);
// Clean up on client disconnect
req.on('close', () => {
clearInterval(intervalId);
});
});
app.listen(3000, () => {
console.log('SSE server running on port 3000');
});
Custom Event Types
Send different types of events:
app.get('/notifications', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Send different event types
function sendEvent(eventType, data) {
res.write(`event: ${eventType}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// Example events
sendEvent('userJoined', { userId: 123, name: 'John' });
sendEvent('message', { text: 'Hello world' });
});
Client Implementation
Basic EventSource Setup
Connect to the SSE endpoint:
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
console.log('Received:', event.data);
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
};
Handling Multiple Event Types
Listen for specific event types:
const eventSource = new EventSource('/notifications');
eventSource.addEventListener('userJoined', (event) => {
const data = JSON.parse(event.data);
console.log('User joined:', data.name);
});
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('New message:', data.text);
});
Real-World Examples
1. Live Dashboard Updates
Implement real-time dashboard updates:
// Server
app.get('/dashboard-events', (req, res) => {
setupSSEHeaders(res);
const metrics = {
users: 0,
transactions: 0,
errors: 0
};
// Update metrics periodically
const intervalId = setInterval(() => {
metrics.users += Math.floor(Math.random() * 10);
metrics.transactions += Math.floor(Math.random() * 100);
res.write(`data: ${JSON.stringify(metrics)}\n\n`);
}, 5000);
req.on('close', () => clearInterval(intervalId));
});
// Client
class DashboardUpdates {
constructor() {
this.eventSource = new EventSource('/dashboard-events');
this.setupEventListeners();
}
setupEventListeners() {
this.eventSource.onmessage = (event) => {
const metrics = JSON.parse(event.data);
this.updateDashboard(metrics);
};
}
updateDashboard(metrics) {
document.getElementById('users').textContent = metrics.users;
document.getElementById('transactions').textContent = metrics.transactions;
}
}
2. Social Feed Updates
Create a real-time social feed:
// Server
class SocialFeedSSE {
constructor() {
this.clients = new Set();
}
addClient(res) {
this.clients.add(res);
res.on('close', () => this.clients.delete(res));
}
broadcast(post) {
this.clients.forEach(client => {
client.write(`data: ${JSON.stringify(post)}\n\n`);
});
}
}
const socialFeed = new SocialFeedSSE();
app.get('/feed-events', (req, res) => {
setupSSEHeaders(res);
socialFeed.addClient(res);
});
app.post('/posts', (req, res) => {
const post = req.body;
// Save post to database
socialFeed.broadcast(post);
res.status(201).json(post);
});
3. Real-Time Analytics
Implement live analytics tracking:
// Server
class AnalyticsTracker {
constructor() {
this.viewers = new Map();
}
addViewer(pageId, res) {
if (!this.viewers.has(pageId)) {
this.viewers.set(pageId, new Set());
}
this.viewers.get(pageId).add(res);
this.broadcastCount(pageId);
res.on('close', () => {
this.viewers.get(pageId).delete(res);
this.broadcastCount(pageId);
});
}
broadcastCount(pageId) {
const count = this.viewers.get(pageId).size;
this.viewers.get(pageId).forEach(client => {
client.write(`data: ${JSON.stringify({ viewers: count })}\n\n`);
});
}
}
Best Practices
1. Error Handling
Implement robust error handling:
class SSEClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.reconnectAttempts = 0;
this.connect();
}
connect() {
this.eventSource = new EventSource(this.url);
this.eventSource.onerror = (error) => {
if (this.eventSource.readyState === EventSource.CLOSED) {
this.reconnect();
}
};
}
reconnect() {
if (this.reconnectAttempts < this.options.maxRetries) {
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, this.options.retryInterval);
}
}
}
2. Connection Management
Handle connections efficiently:
class ConnectionManager {
constructor() {
this.connections = new Map();
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 30000);
}
addConnection(id, res) {
this.connections.set(id, {
res,
timestamp: Date.now()
});
}
cleanup() {
const now = Date.now();
for (const [id, conn] of this.connections) {
if (now - conn.timestamp > 300000) {
conn.res.end();
this.connections.delete(id);
}
}
}
}
3. Performance Optimization
Optimize SSE performance:
class OptimizedSSE {
constructor() {
this.queue = [];
this.flushInterval = setInterval(() => {
this.flush();
}, 100);
}
queueMessage(message) {
this.queue.push(message);
}
flush() {
if (this.queue.length > 0) {
const batch = this.queue.splice(0);
this.broadcast(batch);
}
}
}
Security Considerations
1. Authentication
Secure SSE endpoints:
app.get('/secure-events', authenticateUser, (req, res) => {
setupSSEHeaders(res);
// Only send events relevant to authenticated user
const userId = req.user.id;
subscribeToUserEvents(userId, res);
});
2. Rate Limiting
Implement rate limiting:
const rateLimit = require('express-rate-limit');
const sseRateLimit = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/events', sseRateLimit);
Conclusion
Server-Sent Events provide an efficient solution for real-time, one-way communication in web applications. Key benefits include:
- Simple implementation
- Native browser support
- Automatic reconnection
- Efficient for one-way communication
- Works over HTTP/HTTPS
Remember to:
- Handle errors gracefully
- Manage connections efficiently
- Implement security measures
- Optimize performance
- Monitor connection health
Start implementing SSE in your applications to create engaging real-time features with minimal complexity.