The Real-World Problem
Your Product or Marketing team just assigned you a ticket: “How can we update order counts in real time on the admin dashboard as soon as a customer places an order, without making staff hit refresh?”.
Users have grown accustomed to platforms like Facebook or Shopee pushing real-time notifications effortlessly. To them, building this feature sounds like a five-minute task. Under the hood, however, engineering it efficiently is another story.
Imagine 10,000 operators actively viewing the dashboard. If each browser polls for fresh data every two seconds, your server is slammed with 5,000 requests per second. Over 95% of those requests return empty arrays because no new orders were placed. The result: database connection bottlenecks and massive bandwidth wasted on repetitive HTTP headers.
The go-to solution for many developers is WebSocket. Yet, WebSockets are a double-edged sword.
Why WebSocket Isn’t Always the Right Choice
Traditional HTTP follows a strict request-response model. The server only speaks when spoken to; it cannot proactively push data down to a client unprompted.
WebSocket overcomes this limitation by establishing a persistent, full-duplex communication channel. Think of it like a phone call: both parties can talk and listen simultaneously. However, if your only requirement is streaming order updates from the server to the client, a full-duplex socket is massive overkill.
Maintaining WebSocket connections requires servers to hold stateful open sockets indefinitely, each consuming 20KB to 50KB of RAM. Scaling across multiple containers with Kubernetes or Docker Swarm forces you to configure sticky sessions on Nginx or introduce Redis Pub/Sub to synchronize broadcast events across nodes. Operational complexity and connection drop risks skyrocket.
Comparing Common Real-Time Approaches
Before picking a solution, let’s review the three most common approaches:
1. Short Polling (Continuous Polling)
The browser uses setInterval to send regular HTTP GET requests every 1 to 3 seconds.
- Pros: Extremely easy to implement—up and running in three lines of JavaScript.
- Cons: A massive waste of resources. Every request ships roughly 500 bytes of HTTP headers, even when the payload is an empty array (
[]). Servers quickly hit high CPU loads.
2. Long Polling (Hanging GET)
The client sends a request, and rather than returning immediately, the server holds the connection open until new data arrives or a timeout occurs (typically 30 seconds). Once received, the client immediately initiates a new request.
- Pros: Drastically cuts down on unnecessary empty requests compared to short polling.
- Cons: Re-establishing connections repeatedly still incurs overhead. On unstable networks, managing reconnects and message ordering can easily lead to missed data.
3. WebSocket (Full-Duplex Channel)
Upgrades the HTTP connection to a ws:// or wss:// protocol, enabling bidirectional communication via lightweight binary or text frames.
- Pros: Ultra-low latency (under 10ms), perfectly suited for multiplayer games, chat rooms, or collaborative real-time whiteboards.
- Cons: Uses a non-standard HTTP protocol that corporate firewalls or proxies often block. You must manually implement heartbeat (ping/pong) logic and reconnection mechanisms when connections drop.
The Optimal Solution: Server-Sent Events (SSE)
When your use case only requires a unidirectional stream from server to client (such as notification feeds, server logs, Excel export progress, or live stock tickers), Server-Sent Events (SSE) is the sweet spot.
Technical Advantages of SSE
- Standard HTTP: Operates over standard ports 80/443, effortlessly traversing corporate proxies, load balancers, and firewalls without custom routing rules.
- Native Auto-Reconnect: Browsers handle reconnections automatically when network drops occur. No custom retry loops required.
- Built-in Event ID Tracking: The server can tag events with unique IDs. If a connection drops, the browser automatically sends a
Last-Event-IDheader so the server can backfill missed messages. - HTTP/2 Multiplexing: Under HTTP/1.1, browsers restrict concurrent SSE connections to six per domain. With HTTP/2 multiplexing, you can run hundreds of concurrent event streams over a single TCP connection.
Step 1: Building the SSE Endpoint with Node.js & Express
Initialize the project directory and install the required dependencies:
mkdir sse-demo && cd sse-demo
npm init -y
npm install express cors
Create a server.js file. The most crucial part is configuring the three required HTTP response headers:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/api/realtime-feed', (req, res) => {
// 1. Configure mandatory headers for the SSE standard
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Flush headers to the client immediately to prevent Express from buffering
res.flushHeaders();
// Send a welcome event upon successful connection
res.write(`data: ${JSON.stringify({ message: 'SSE connection established successfully' })}\n\n`);
// Simulate pushing a new order notification every 3 seconds
const timerId = setInterval(() => {
const payload = {
orderId: 'ORD-' + Math.floor(1000 + Math.random() * 9000),
amount: (Math.random() * 500).toFixed(2),
timestamp: new Date().toLocaleTimeString('en-US')
};
// Mandatory format: starts with "data: " and ends with two newlines "\n\n"
res.write(`data: ${JSON.stringify(payload)}\n\n`);
}, 3000);
// 2. Clean up resources when the client closes the tab or navigates away
req.on('close', () => {
clearInterval(timerId);
res.end();
console.log('Client disconnected. Resources released.');
});
});
app.listen(5000, () => {
console.log('SSE server listening on port 5000');
});
Step 2: Consuming Events on the React Client
You don’t need to install socket.io-client or any third-party libraries. Simply use the browser’s native EventSource Web API:
import React, { useState, useEffect } from 'react';
function DashboardOrders() {
const [orders, setOrders] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('Connecting...');
useEffect(() => {
// Initialize EventSource pointing to the server
const sse = new EventSource('http://localhost:5000/api/realtime-feed');
sse.onopen = () => {
setConnectionStatus('Connected');
};
// Listen for messages from the server
sse.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.orderId) {
setOrders((prev) => [data, ...prev.slice(0, 9)]);
}
} catch (err) {
console.error('JSON parse error:', err);
}
};
// The browser will automatically attempt to reconnect after a few seconds if an issue occurs
sse.onerror = (err) => {
console.error('SSE connection error:', err);
setConnectionStatus('Connection lost. Retrying...');
};
// Cleanup: Close the connection when the component unmounts to prevent memory leaks
return () => {
sse.close();
};
}, []);
return (
<div style={{ padding: '24px', fontFamily: 'system-ui, sans-serif' }}>
<h2>Live Incoming Orders</h2>
<p>Status: <strong>{connectionStatus}</strong></p>
<ul>
{orders.map((item, idx) => (
<li key={idx} style={{ marginBottom: '8px' }}>
<span style={{ color: '#666' }}>[{item.timestamp}]</span> Order ID: <strong>{item.orderId}</strong> — Total: <span style={{ color: '#0d9488', fontWeight: 'bold' }}>${item.amount}</span>
</li>
))}
</ul>
</div>
);
}
export default DashboardOrders;
Technical Comparison: SSE vs. WebSocket
- Directionality: SSE is strictly unidirectional (Server to Client). WebSocket is bidirectional (Client and Server are peers).
- Underlying Protocol: SSE runs over standard HTTP and fully leverages HTTP/2 multiplexing. WebSocket establishes a dedicated TCP-based protocol (ws:// or wss://).
- Automatic Reconnection: SSE includes native browser support for auto-reconnect and event ID tracking. WebSocket requires you to manually implement reconnection logic and offline message queuing.
- Data Formats: SSE transmits UTF-8 text streams (commonly formatted as JSON). WebSocket supports both text and binary formats (Binary, ArrayBuffer).
Don’t over-engineer your architecture before your use case demands it. Unless your application requires real-time bidirectional communication with millisecond latencies—such as multiplayer games or video calls—Server-Sent Events offers the cleanest, most maintainable, and resource-efficient solution.

