
Why Are You Still Managing WebSocket Heartbeats When Server-Sent Events Exist?
You might be over-engineering your real-time features with stateful WebSockets when the browser's native, auto-reconnecting Server-Sent Events can handle the job with half the complexity.
Most developers reach for WebSockets the second someone mentions the word "real-time," but that’s like hiring a private investigator when all you needed was a doorbell camera. If you are building a notification system, a live dashboard, or a ticker, you are likely over-complicating your stack by managing stateful, bi-directional sockets when Server-Sent Events (SSE) could do the heavy lifting for a fraction of the mental overhead.
The "WebSocket Tax" You're Paying
WebSockets are great for things like multiplayer gaming or high-frequency trading where milliseconds matter in both directions. But for 90% of web apps, they are a massive pain in the neck.
When you use WebSockets, you're responsible for:
1. Heartbeats/Pings: Ensuring the connection hasn't silently died while idle.
2. Reconnection Logic: Handling what happens when the user goes through a tunnel or their Wi-Fi hiccups.
3. Load Balancing: WebSockets are stateful. Your load balancer needs to support "sticky sessions," or you need a complex Pub/Sub backend like Redis to sync messages across server instances.
Server-Sent Events (SSE) throws all that garbage out the window. It’s just an HTTP connection that stays open. The browser handles the reconnection automatically. No heartbeats required.
The "Hello World" of Simplicity
Let's look at how little code it takes to get a stream running. On the server side (using Node/Express), you just need to set a few headers and keep the response alive.
// server.js
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
// Essential headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send an initial heartbeat or data
res.write(`data: ${JSON.stringify({ message: 'Connected!' })}\n\n`);
// Simulate sending updates every 3 seconds
const interval = setInterval(() => {
const data = { time: new Date().toLocaleTimeString(), status: 'OK' };
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 3000);
// Clean up when the client disconnects
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000, () => console.log('SSE server running on port 3000'));Notice the format: data: <your-string>\n\n. That double newline is crucial; it tells the browser the message is complete.
The Frontend is even easier
The browser has a native EventSource API specifically for this. You don't need a library. You don't need socket.io. You just need this:
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('New update from server:', data);
};
eventSource.onerror = (err) => {
console.error('EventSource failed:', err);
// Note: EventSource will automatically try to reconnect by default!
};If the server goes down, the browser will wait a few seconds and try to reconnect. It keeps trying until the server comes back. You don't have to write a single line of setTimeout logic to handle "retrying connection..." states.
Why SSE is the "Smart Choice" for 2024
The web has changed. We have HTTP/2 and HTTP/3 now.
In the old days of HTTP/1.1, browsers had a limit of about 6 concurrent connections to a single domain. If you opened 6 tabs of your SSE-powered app, the 7th tab would hang. This was a dealbreaker.
But with HTTP/2, that limit is gone. Multiplexing allows hundreds of requests (including SSE streams) to live on a single TCP connection. If your infrastructure supports HTTP/2 (and it really should by now), the "connection limit" argument against SSE is dead.
When should you actually use WebSockets?
I’m not a hater; WebSockets have their place. You should stick with WebSockets if:
- Binary Data: You are streaming raw binary data (like audio or video frames). SSE is text-only (though you could Base64 encode stuff, please don't).
- True Bi-directionality: You need the client to push data to the server at a high frequency (like a drawing app or a game).
- Latency is everything: If 50ms vs 100ms is the difference between life and death.
For everything else—social media feeds, stock tickers, build logs, and "Order is being prepared" notifications—SSE is objectively less work.
The "Gotcha" (Because there's always one)
If you're using a proxy like Nginx, you might find that your events don't show up immediately. Nginx likes to buffer responses to be "efficient." You’ll need to turn that off for your event endpoint:
location /events {
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
}Wrapping Up
Next time you're about to npm install socket.io, take five minutes to ask yourself: "Does the client actually need to talk to the server over this socket?"
If the answer is no, save yourself the headache. Use Server-Sent Events. It’s built-in, it’s lighter on your server, and your future self—the one who doesn't have to debug a zombie WebSocket connection at 2 AM—will thank you.


