WebSockets & Real-Time Apps
Server-Sent Events (SSE)
Server-Sent Events (SSE)
Server-Sent Events (SSE) is a standard allowing servers to push data to web clients over HTTP. Unlike WebSockets, SSE is unidirectional (server to client only) and works over traditional HTTP connections.
SSE vs WebSockets
Understanding when to use each technology:
| Feature | SSE | WebSockets |
|---|---|---|
| Direction | Server to client only | Bidirectional |
| Protocol | HTTP | WebSocket protocol |
| Reconnection | Automatic | Manual |
| Data Format | Text only (UTF-8) | Text and binary |
| Browser Support | All modern browsers | All modern browsers |
| Complexity | Simple | More complex |
When to Use SSE: News feeds, stock tickers, notifications, live scores, server monitoring dashboards - any scenario where the server needs to push updates but clients don't need to send frequent messages back.
EventSource API
The client-side API for SSE is straightforward:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Example</title>
</head>
<body>
<div id="notifications"></div>
<script>
// Create EventSource connection
const eventSource = new EventSource('/api/events');
// Listen for messages
eventSource.onmessage = (event) => {
console.log('New message:', event.data);
displayNotification(event.data);
};
// Listen for connection open
eventSource.onopen = () => {
console.log('Connection opened');
};
// Listen for errors
eventSource.onerror = (error) => {
console.error('EventSource error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};
function displayNotification(message) {
const div = document.createElement('div');
div.textContent = message;
document.getElementById('notifications').appendChild(div);
}
// Close connection when needed
// eventSource.close();
</script>
</body>
</html>
Creating SSE Endpoint in Express
Server-side implementation with Node.js and Express:
const express = require('express');
const app = express();
// SSE endpoint
app.get('/api/events', (req, res) => {
// Set headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Optional: Enable CORS
res.setHeader('Access-Control-Allow-Origin', '*');
// Send initial comment (helps keep connection alive)
res.write(':ok\n\n');
// Send a message every 5 seconds
const intervalId = setInterval(() => {
const data = {
timestamp: new Date().toISOString(),
message: 'Hello from server'
};
// Format: data: JSON\n\n
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// Clean up on client disconnect
req.on('close', () => {
clearInterval(intervalId);
console.log('Client disconnected');
});
});
app.listen(3000, () => {
console.log('SSE server running on port 3000');
});
Note: The SSE message format is strict:
data: message\n\n. The double newline (\n\n) indicates the end of a message.
Sending Events with Event Types
SSE supports custom event types for different message categories:
// Server-side: Send different event types
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 notification event
function sendNotification(message) {
res.write(`event: notification\n`);
res.write(`data: ${JSON.stringify(message)}\n\n`);
}
// Send update event
function sendUpdate(data) {
res.write(`event: update\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// Send alert event
function sendAlert(alert) {
res.write(`event: alert\n`);
res.write(`data: ${JSON.stringify(alert)}\n\n`);
}
// Example: Send different events
setInterval(() => {
sendNotification({ text: 'New comment on your post' });
}, 10000);
setInterval(() => {
sendUpdate({ users: 42, requests: 1523 });
}, 5000);
req.on('close', () => {
console.log('Client disconnected');
});
});
// Client-side: Listen to specific event types
const eventSource = new EventSource('/api/events');
// Listen for notification events
eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
console.log('Notification:', data.text);
showNotification(data.text);
});
// Listen for update events
eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
console.log('Update:', data);
updateDashboard(data);
});
// Listen for alert events
eventSource.addEventListener('alert', (event) => {
const data = JSON.parse(event.data);
console.log('Alert:', data);
showAlert(data);
});
// Generic message handler (for events without type)
eventSource.onmessage = (event) => {
console.log('Generic message:', event.data);
};
Automatic Reconnection
SSE automatically reconnects when the connection drops:
// Server: Send retry interval (in milliseconds)
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Set retry interval to 3 seconds
res.write('retry: 3000\n\n');
// Send events...
});
// Client: Handle reconnection
const eventSource = new EventSource('/api/events');
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
eventSource.onerror = (error) => {
if (eventSource.readyState === EventSource.CONNECTING) {
reconnectAttempts++;
console.log(`Reconnecting... (attempt ${reconnectAttempts})`);
if (reconnectAttempts >= maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
eventSource.close();
// Show user notification
alert('Lost connection to server. Please refresh.');
}
} else if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};
eventSource.onopen = () => {
reconnectAttempts = 0;
console.log('Connection (re)established');
};
Last Event ID for Resuming
Use event IDs to resume from the last received event after reconnection:
// Server: Send events with IDs
let eventId = 0;
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Get last event ID from client (if reconnecting)
const lastEventId = req.headers['last-event-id'];
console.log('Last event ID received:', lastEventId);
// Send events with IDs
const intervalId = setInterval(() => {
eventId++;
res.write(`id: ${eventId}\n`);
res.write(`data: ${JSON.stringify({
id: eventId,
message: 'Event ' + eventId
})}\n\n`);
}, 2000);
req.on('close', () => {
clearInterval(intervalId);
});
});
// Client: EventSource automatically sends last event ID on reconnect
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
console.log('Event ID:', event.lastEventId);
console.log('Data:', event.data);
// Store last event ID if needed
localStorage.setItem('lastEventId', event.lastEventId);
};
Best Practice: Always send event IDs for critical data streams. This allows clients to resume from the last received event after a disconnection, preventing data loss.
SSE Use Cases
Ideal scenarios for Server-Sent Events:
- Real-time notifications: User notifications, alerts, system messages
- Live feeds: News updates, social media feeds, activity streams
- Monitoring dashboards: Server metrics, application logs, analytics
- Stock tickers: Live stock prices, cryptocurrency rates
- Sports scores: Live game updates, score changes
- Progress tracking: File uploads, long-running tasks
- IoT data streams: Sensor readings, device status updates
Real-World Example: Live Notifications
// Server: Notification system
const express = require('express');
const app = express();
// Store active connections
const clients = new Map();
// SSE endpoint
app.get('/api/notifications', (req, res) => {
const userId = req.query.userId;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Add client to active connections
clients.set(userId, res);
// Send initial connection confirmation
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
// Keep-alive ping every 30 seconds
const keepAliveId = setInterval(() => {
res.write(':ping\n\n');
}, 30000);
req.on('close', () => {
clearInterval(keepAliveId);
clients.delete(userId);
console.log(`User ${userId} disconnected`);
});
});
// API endpoint to send notification
app.post('/api/send-notification', express.json(), (req, res) => {
const { userId, notification } = req.body;
const client = clients.get(userId);
if (client) {
client.write(`event: notification\n`);
client.write(`data: ${JSON.stringify(notification)}\n\n`);
res.json({ success: true });
} else {
res.status(404).json({ error: 'User not connected' });
}
});
app.listen(3000);
// Client: Notification UI
const userId = 'user123';
const eventSource = new EventSource(`/api/notifications?userId=${userId}`);
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
displayNotification(notification);
});
function displayNotification(notification) {
// Show browser notification
if (Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message,
icon: notification.icon
});
}
// Show in-app notification
const notifDiv = document.createElement('div');
notifDiv.className = 'notification';
notifDiv.innerHTML = `
<strong>${notification.title}</strong>
<p>${notification.message}</p>
`;
document.getElementById('notifications').appendChild(notifDiv);
// Auto-dismiss after 5 seconds
setTimeout(() => notifDiv.remove(), 5000);
}
SSE Limitations
Important Limitations:
- Browser connection limit: Most browsers limit SSE connections to 6 per domain
- No binary data: SSE only supports UTF-8 text (WebSockets support binary)
- One-way only: Client cannot send data through SSE connection
- No mobile background: Mobile browsers may close connections when app is backgrounded
- HTTP/1.1 limitation: Each SSE connection uses one HTTP connection (better with HTTP/2)
Exercise: Build a Live Blog
Create a Real-Time Blog with SSE:
- Build an Express server with SSE endpoint for blog posts
- Create a client page that displays blog posts as they are published
- Add different event types: 'post' for new posts, 'update' for edits, 'delete' for removals
- Implement automatic reconnection with visual indicator
- Use event IDs to handle reconnection gracefully
- Add a simple admin interface to publish posts
- Bonus: Show real-time view count for each post
Summary
- SSE provides simple server-to-client real-time communication over HTTP
- Use EventSource API on the client side - it's built into all modern browsers
- SSE automatically reconnects when connections drop
- Event types allow organizing different message categories
- Event IDs enable resuming from last received message
- SSE is ideal for notifications, feeds, and monitoring dashboards
- Choose SSE over WebSockets when you only need server-to-client communication
- Be aware of browser connection limits (6 per domain)