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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)