Express Server-Sent Events
In modern web applications, real-time updates have become increasingly important. Users expect to see data changes instantly without manually refreshing the page. While WebSockets provide bidirectional communication, Server-Sent Events (SSE) offer a simpler alternative when you only need server-to-client communication.
What are Server-Sent Events?
Server-Sent Events (SSE) is a standard that enables servers to push real-time updates to clients over a single HTTP connection. Unlike WebSockets, SSE is:
- Unidirectional: Data flows only from server to client
- Built on HTTP: No special protocol required
- Automatically reconnects: Browsers handle reconnection on disconnection
- Text-based: Uses a simple format for messages
When to Use SSE
Server-Sent Events are ideal for:
- Live news updates
- Stock tickers
- Social media feeds
- Notifications
- Any scenario where you need server-to-client updates without client-to-server communication
Setting Up SSE in Express
Let's implement a basic SSE setup in an Express application:
const express = require('express');
const app = express();
app.get('/events', function(req, res) {
// Set headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send an initial message
res.write('data: Connected to event stream\n\n');
// Send a message every 5 seconds
const intervalId = setInterval(() => {
const data = {
time: new Date().toTimeString(),
message: 'Server update'
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// Clean up when client disconnects
req.on('close', () => {
clearInterval(intervalId);
});
});
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Now, let's create a simple HTML file to consume these events:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Demo</title>
<style>
#events {
height: 300px;
border: 1px solid #ccc;
margin: 20px 0;
padding: 10px;
overflow-y: scroll;
}
</style>
</head>
<body>
<h1>Server-Sent Events Demo</h1>
<div id="events"></div>
<script>
const eventsDiv = document.getElementById('events');
// Create an EventSource instance pointing to our events endpoint
const eventSource = new EventSource('/events');
// Listen for messages from the server
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
const newElement = document.createElement('div');
newElement.innerHTML = ``;
eventsDiv.appendChild(newElement);
// Auto-scroll to bottom
eventsDiv.scrollTop = eventsDiv.scrollHeight;
};
// Handle connection open
eventSource.onopen = function() {
console.log('Connection to server opened');
};
// Handle errors
eventSource.onerror = function() {
const newElement = document.createElement('div');
newElement.innerHTML = '<strong>Error connecting to server</strong>';
eventsDiv.appendChild(newElement);
};
</script>
</body>
</html>
Understanding the SSE Format
Server-Sent Events use a simple text format:
- Each message consists of one or more field/value pairs
- Each field/value pair is formatted as
field: value
followed by a newline - Messages are separated by two newlines (
\n\n
)
Common fields include:
data
: The message payload (required)event
: Custom event nameid
: Message ID for reconnectionretry
: Reconnection time in milliseconds
Advanced SSE Techniques
Custom Event Types
You can send different types of events that clients can listen for specifically:
// Server-side
res.write('event: userConnected\n');
res.write(`data: {"username": "john"}\n\n`);
// Later, send a different event type
res.write('event: newMessage\n');
res.write(`data: {"text": "Hello world"}\n\n`);
On the client:
// Client-side
eventSource.addEventListener('userConnected', function(e) {
const data = JSON.parse(e.data);
console.log(`User connected: ${data.username}`);
});
eventSource.addEventListener('newMessage', function(e) {
const data = JSON.parse(e.data);
console.log(`New message: ${data.text}`);
});
Message IDs for Reconnection
You can add IDs to messages to allow the browser to resume from where it left off after a disconnection:
let messageId = 0;
// In your event handler
res.write(`id: ${messageId}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
messageId++;
Setting Reconnection Time
You can control how quickly the browser attempts to reconnect:
res.write('retry: 10000\n'); // Reconnect after 10 seconds
res.write(`data: ${JSON.stringify(data)}\n\n`);
Real-World Example: Live Dashboard
Let's create a more practical example - a server monitoring dashboard that displays real-time system metrics:
const express = require('express');
const os = require('os');
const app = express();
// Store connected clients
const clients = new Set();
app.get('/dashboard-events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Add this client to our connected clients set
clients.add(res);
// Send initial connection message
res.write('event: connected\n');
res.write(`data: ${JSON.stringify({message: "Connected to system monitor"})}\n\n`);
// Handle client disconnection
req.on('close', () => {
clients.delete(res);
console.log('Client disconnected');
});
});
app.use(express.static('public'));
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
// Broadcast system metrics to all connected clients
function broadcastMetrics() {
const metrics = {
time: new Date().toISOString(),
cpuLoad: os.loadavg()[0],
freeMemory: os.freemem() / 1024 / 1024, // MB
uptime: os.uptime()
};
clients.forEach(client => {
client.write('event: metrics\n');
client.write(`data: ${JSON.stringify(metrics)}\n\n`);
});
}
// Send metrics every 2 seconds
setInterval(broadcastMetrics, 2000);
And here's the client-side HTML file (located in a public
folder):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>System Dashboard</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.dashboard { border: 1px solid #ddd; border-radius: 4px; padding: 20px; margin-top: 20px; }
.metric { margin-bottom: 15px; }
.metric-name { font-weight: bold; }
.metric-value { font-size: 1.2em; }
#connection-status { color: green; }
#time { font-size: 0.8em; color: #666; }
</style>
</head>
<body>
<h1>System Dashboard</h1>
<div id="connection-status">Connecting...</div>
<div id="time"></div>
<div class="dashboard">
<div class="metric">
<div class="metric-name">CPU Load (1 min average)</div>
<div class="metric-value" id="cpu-load">-</div>
</div>
<div class="metric">
<div class="metric-name">Free Memory</div>
<div class="metric-value" id="free-memory">-</div>
</div>
<div class="metric">
<div class="metric-name">Uptime</div>
<div class="metric-value" id="uptime">-</div>
</div>
</div>
<script>
const eventSource = new EventSource('/dashboard-events');
eventSource.addEventListener('connected', function(e) {
const data = JSON.parse(e.data);
document.getElementById('connection-status').textContent = data.message;
});
eventSource.addEventListener('metrics', function(e) {
const metrics = JSON.parse(e.data);
// Update the dashboard
document.getElementById('cpu-load').textContent = metrics.cpuLoad.toFixed(2);
document.getElementById('free-memory').textContent =
`${Math.round(metrics.freeMemory)} MB`;
document.getElementById('uptime').textContent =
`${Math.floor(metrics.uptime / 60 / 60)} hours, ${Math.floor(metrics.uptime / 60) % 60} minutes`;
document.getElementById('time').textContent =
`Last updated: ${new Date(metrics.time).toLocaleTimeString()}`;
});
eventSource.onerror = function() {
document.getElementById('connection-status').textContent = 'Disconnected. Reconnecting...';
document.getElementById('connection-status').style.color = 'red';
};
</script>
</body>
</html>
SSE vs. WebSockets
When deciding between SSE and WebSockets, consider:
Feature | SSE | WebSockets |
---|---|---|
Communication | One-way (server to client) | Two-way |
Protocol | HTTP | WebSocket protocol (ws:// or wss://) |
Reconnection | Automatic | Must be implemented manually |
Message Types | Text only | Text and binary |
Browser Support | All modern browsers | All modern browsers |
Max Connections | Limited by browser (typically 6 per domain) | Higher limit |
Implementation | Simpler | More complex |
Common Issues and Solutions
Browser Connection Limits
Browsers typically limit the number of concurrent SSE connections to the same domain (usually 6). To work around this:
- Use multiple subdomains for SSE endpoints
- Implement a fallback mechanism using long polling
- Consider WebSockets for high-connection scenarios
Keeping Connections Alive
Some proxies might close idle connections. To prevent this:
- Send regular heartbeat messages (empty comments)
- Configure your proxy to allow longer-lived connections
// Send a comment as heartbeat every 30 seconds
const heartbeatInterval = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
req.on('close', () => {
clearInterval(heartbeatInterval);
});
Summary
Server-Sent Events provide an elegant solution for real-time server-to-client communication in Express applications. They're simpler to implement than WebSockets when you only need one-way communication, and they come with built-in features like automatic reconnection.
Key points to remember:
- SSE uses standard HTTP and is text-based
- Ideal for scenarios requiring server-to-client updates
- Automatically handles reconnection
- Limited to 6 connections per browser domain
- Can send different event types for more structured data
By leveraging SSE in your Express applications, you can create responsive, real-time user experiences with minimal overhead.
Additional Resources
- MDN Web Docs on Server-Sent Events
- HTML5 SSE Specification
- Using server-sent events - Web APIs | MDN
Exercises
- Modify the dashboard example to include additional system metrics like total memory, number of CPUs, or network interfaces.
- Create a live chat application where new messages are pushed to clients using SSE.
- Implement a stock ticker that sends updates for different stocks as different event types.
- Add error handling to handle network issues and create a reconnection strategy.
- Build a notification system that filters messages based on user preferences stored on the client side.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)