Server-Sent Events (SSE) provide a powerful way to implement real-time updates in web applications. Unlike WebSockets, SSE offers a simpler, unidirectional communication channel that's perfect for scenarios where you need server-to-client updates.
Understanding Server-Sent Events
SSE enables servers to push data to web clients over HTTP. It's ideal for:
- Live feeds
- Real-time notifications
- Status updates
- Analytics dashboards
Client-Side Implementation
Here's how to connect to an SSE endpoint:
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
updateUI(data)
}
eventSource.addEventListener('custom-event', (event) => {
handleCustomEvent(event.data)
})
eventSource.onerror = (error) => {
console.error('SSE error:', error)
eventSource.close()
}
Server Implementation with Node.js
Here's a basic Express server implementing SSE:
const express = require('express')
const app = express()
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
// Send initial connection message
res.write('data: {"connection": "established"}\n\n')
// Store connection for cleanup
const clientId = Date.now()
const clients = new Map()
clients.set(clientId, res)
req.on('close', () => {
clients.delete(clientId)
})
})
function broadcastToAll(data) {
clients.forEach(client => {
client.write(`data: ${JSON.stringify(data)}\n\n`)
})
}
Custom Event Types
You can send different types of events:
// Server-side
function sendEvent(res, eventType, data) {
res.write(`event: ${eventType}\n`)
res.write(`data: ${JSON.stringify(data)}\n\n`)
}
// Usage
sendEvent(res, 'update', { status: 'processing' })
sendEvent(res, 'complete', { status: 'done', result: 'success' })
Implementing Retry Logic
Add robust retry handling:
const eventSource = new EventSource('/api/events', {
withCredentials: true
})
let retryCount = 0
const maxRetries = 5
function connect() {
eventSource.onopen = () => {
console.log('Connection established')
retryCount = 0
}
eventSource.onerror = (error) => {
if (retryCount >= maxRetries) {
console.error('Max retries reached')
eventSource.close()
return
}
retryCount++
const timeout = Math.min(1000 * Math.pow(2, retryCount), 10000)
setTimeout(() => {
console.log(`Retrying connection (${retryCount}/${maxRetries})`)
connect()
}, timeout)
}
}
connect()
Real-World Example: Live Dashboard
Here's a complete example of a live dashboard:
// Server (Node.js/Express)
const express = require('express')
const app = express()
const clients = new Set()
let metrics = {
activeUsers: 0,
requestsPerMinute: 0,
errorRate: 0
}
app.get('/api/dashboard/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
clients.add(res)
req.on('close', () => {
clients.delete(res)
})
// Send initial data
res.write(`data: ${JSON.stringify(metrics)}\n\n`)
})
// Update metrics periodically
setInterval(() => {
metrics = {
activeUsers: Math.floor(Math.random() * 1000),
requestsPerMinute: Math.floor(Math.random() * 5000),
errorRate: Math.random() * 5
}
clients.forEach(client => {
client.write(`data: ${JSON.stringify(metrics)}\n\n`)
})
}, 5000)
// Client-side JavaScript
class DashboardUI {
constructor() {
this.eventSource = new EventSource('/api/dashboard/events')
this.setupEventListeners()
}
setupEventListeners() {
this.eventSource.onmessage = (event) => {
const metrics = JSON.parse(event.data)
this.updateDashboard(metrics)
}
}
updateDashboard(metrics) {
document.getElementById('activeUsers').textContent =
metrics.activeUsers.toLocaleString()
document.getElementById('rpm').textContent =
metrics.requestsPerMinute.toLocaleString()
document.getElementById('errorRate').textContent =
`${metrics.errorRate.toFixed(2)}%`
}
}
new DashboardUI()
Performance Considerations
- Connection Limits
- Monitor active connections
- Implement connection pooling
- Set appropriate timeouts
- Memory Management
- Clean up connections properly
- Monitor memory usage
- Implement garbage collection strategies
Example connection manager:
class ConnectionManager {
constructor(maxConnections = 1000) {
this.connections = new Map()
this.maxConnections = maxConnections
}
addConnection(id, res) {
if (this.connections.size >= this.maxConnections) {
throw new Error('Max connections reached')
}
this.connections.set(id, {
response: res,
timestamp: Date.now()
})
}
removeConnection(id) {
this.connections.delete(id)
}
cleanup() {
const now = Date.now()
const timeout = 30 * 60 * 1000 // 30 minutes
this.connections.forEach((conn, id) => {
if (now - conn.timestamp > timeout) {
conn.response.end()
this.removeConnection(id)
}
})
}
}
Security Best Practices
- Authentication
- Implement token-based auth
- Validate connections
- Use HTTPS
- Rate Limiting
- Implement per-client limits
- Monitor for abuse
- Add request throttling
Example security middleware:
function sseAuthMiddleware(req, res, next) {
const token = req.headers.authorization
if (!token || !validateToken(token)) {
res.status(401).end()
return
}
next()
}
app.get('/api/events', sseAuthMiddleware, (req, res) => {
// SSE handler code
})
Conclusion
Server-Sent Events provide a powerful, efficient way to implement real-time features in web applications. They offer several advantages over WebSockets for unidirectional communication:
- Simpler implementation
- Automatic reconnection
- Built-in event IDs
- Standard HTTP protocol
By following these patterns and best practices, you can build robust, scalable real-time applications that provide immediate updates to your users while maintaining performance and security.