Skip to main content

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:

javascript
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:

html
<!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 = `<strong>${data.time}</strong>: ${data.message}`;
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 name
  • id: Message ID for reconnection
  • retry: Reconnection time in milliseconds

Advanced SSE Techniques

Custom Event Types

You can send different types of events that clients can listen for specifically:

javascript
// 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:

javascript
// 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:

javascript
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:

javascript
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:

javascript
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):

html
<!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:

FeatureSSEWebSockets
CommunicationOne-way (server to client)Two-way
ProtocolHTTPWebSocket protocol (ws:// or wss://)
ReconnectionAutomaticMust be implemented manually
Message TypesText onlyText and binary
Browser SupportAll modern browsersAll modern browsers
Max ConnectionsLimited by browser (typically 6 per domain)Higher limit
ImplementationSimplerMore 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
javascript
// 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

Exercises

  1. Modify the dashboard example to include additional system metrics like total memory, number of CPUs, or network interfaces.
  2. Create a live chat application where new messages are pushed to clients using SSE.
  3. Implement a stock ticker that sends updates for different stocks as different event types.
  4. Add error handling to handle network issues and create a reconnection strategy.
  5. 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! :)