The Overhead of Full-Duplex Communication
Most developers instinctively reach for WebSockets the moment a project requirement mentions the words “real-time.” I used to be one of them. We picture a persistent, bidirectional pipe where data flows freely between the client and server. It sounds perfect on paper, but in practice, for features like notifications, activity feeds, or live dashboards, WebSockets often introduce more complexity than they solve.
When I was building a notification system for a high-traffic e-commerce platform, I initially went the WebSocket route. Quickly, I ran into hurdles: managing heartbeats to keep connections alive, handling complex reconnection logic on the client, and configuring Nginx to properly handle the protocol upgrade from HTTP to WS. My infrastructure team was worried about the memory footprint of keeping thousands of idle, bidirectional connections open. We were using a sledgehammer to crack a nut.
Why We Overcomplicate Real-time Features
The root cause of this complexity is a misunderstanding of communication needs. Most real-time features in web applications are unidirectional. A notification occurs on the server—a new message, a processed payment, a status change—and needs to be pushed to the user. The user rarely needs to send data back over that same persistent connection.
WebSockets are designed for bidirectional, low-latency communication (like a multiplayer game or a collaborative document editor). Using them for simple notifications forces you to manage a full-duplex protocol when a simple stream would suffice. Furthermore, WebSockets don’t follow standard HTTP semantics; they require specific proxy configurations and can be blocked by certain firewalls or corporate proxies that don’t recognize the Upgrade header.
Server-Sent Events: The Lightweight Contender
Server-Sent Events (SSE) offer a much cleaner path. Unlike WebSockets, SSE operates entirely over standard HTTP. It uses a long-lived HTTP connection where the server keeps the response open and sends data in a specific format (text/event-stream).
The beauty of SSE lies in its simplicity. Because it’s just HTTP, it works out of the box with your existing load balancers, firewalls, and authentication logic.
The browser’s EventSource API handles the most annoying part of real-time programming: automatic reconnection. If the connection drops, the browser tries to reconnect without you writing a single line of setInterval or backoff logic. I have applied this approach in production and the results have been consistently stable, especially in terms of resource management and ease of maintenance.
Hands-on: Building a Notification System with Node.js
To demonstrate how simple this is, let’s build a minimal notification server using Node.js and Express. We will create an endpoint that clients can subscribe to and a secondary endpoint to “trigger” notifications.
1. Setting up the Server
First, initialize a project and install Express:
mkdir sse-notifications
cd sse-notifications
npm init -y
npm install express
Now, create server.js. This script manages a list of connected clients and pushes data to them when a new event occurs.
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static('public'));
// Store active client connections
let clients = [];
app.get('/events', (req, res) => {
// Mandatory headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Register the client
const clientId = Date.now();
const newClient = {
id: clientId,
res
};
clients.push(newClient);
// Remove client when connection closes
req.on('close', () => {
clients = clients.filter(client => client.id !== clientId);
});
});
// Endpoint to trigger a notification
app.post('/notify', (req, res) => {
const message = req.body.message || 'New notification!';
// Send data to all connected clients
clients.forEach(client => {
client.res.write(`data: ${JSON.stringify({ message, time: new Date() })}\n\n`);
});
res.status(200).send({ success: true });
});
app.listen(PORT, () => {
console.log(`SSE Server running on http://localhost:${PORT}`);
});
2. Creating the Client
The client-side implementation is even simpler. We don’t need any external libraries; the EventSource API is native to all modern browsers.
Create a public/index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Notifications</title>
</head>
<body>
<h1>Live Notifications</h1>
<ul id="notif-list"></ul>
<script>
const eventSource = new EventSource('/events');
const list = document.getElementById('notif-list');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
const li = document.createElement('li');
li.textContent = `[${new Date(data.time).toLocaleTimeString()}] ${data.message}`;
list.prepend(li);
};
eventSource.onerror = (err) => {
console.error("EventSource failed:", err);
};
</script>
</body>
</html>
3. Testing the System
Start your server:
node server.js
Open http://localhost:3000 in multiple browser tabs. Then, use curl or a tool like Postman to trigger a notification:
curl -X POST http://localhost:3000/notify \
-H "Content-Type: application/json" \
-d '{"message": "Hello from the server!"}'
You’ll see the notification appear instantly in all open tabs. No complex handshake, no socket management, just pure HTTP streaming.
Scaling and Production Reality
When moving SSE to production, there are two main things I always keep in mind. First is the browser’s connection limit. Most browsers limit the number of open HTTP/1.1 connections to the same domain to 6. If a user opens 7 tabs of your app, the 7th will fail to connect. The solution is simple: use HTTP/2. Under HTTP/2, these connections are multiplexed, allowing many more streams over a single TCP connection.
The second consideration is keeping the connection alive during inactivity. Some proxies or load balancers might kill an “idle” HTTP connection if no data is sent for 30 or 60 seconds. My trick is to implement a “heartbeat”—sending a small comment line (: keep-alive\n\n) every 15 seconds. SSE ignores lines starting with a colon, so it keeps the pipe open without triggering any event on the client.
Choosing the Right Tool
WebSockets aren’t obsolete, but they are specialized. If you’re building a chat application where users are constantly sending messages back and forth, or a real-time collaborative whiteboard, stick with WebSockets. The overhead is justified there.
However, for 90% of real-time needs—like notifying a user that their report is ready, updating a progress bar, or pushing a live price change—SSE is the superior architectural choice. It is easier to debug (you can see the stream in the Network tab of Chrome DevTools), easier to scale, and far simpler to implement. Next time you’re tempted to reach for Socket.io, take a look at your requirements. If the data flow is primarily one-way, save yourself the headache and use Server-Sent Events.

