There is a moment in every web developer's career when a client asks: "Can we make this update in real time?"
Your mind immediately jumps to WebSockets. It is the industry buzzword. It sounds fast. You spin up socket.io or Reverb, spend two days fighting with your load balancer, and finally get it working.
But here is the harsh truth: for about 90% of modern web applications—including AI chat streaming, live dashboards, and notification feeds—WebSockets are massive overkill.
Instead, you should probably be using Server-Sent Events (SSE). Here is why SSE is often the cleaner, cheaper, and more pragmatic choice.
The Core Difference
Both WebSockets and SSE exist to push data from the server to the client without the client needing to constantly poll the server.
- WebSockets create a full-duplex, persistent TCP connection. Both the client and the server can shout at each other simultaneously.
- SSE is a unidirectional, HTTP-based stream. The server keeps a standard HTTP connection open and pushes text-based events down to the client.
Why WebSockets Are a Headache in Production
WebSockets are amazing for multiplayer games or collaborative tools like Google Docs where clients are constantly sending high-frequency data back to the server. But that power comes with a heavy infrastructure tax.
- Stateful Scaling: WebSockets are stateful. If you scale horizontally, your load balancer needs connection-aware routing (sticky sessions) to ensure a client's subsequent messages go to the specific server holding their connection.
- Proxy Nightmares: Aggressive corporate proxies and firewalls frequently drop WebSocket protocol upgrades, leaving connections in failure modes that are notoriously hard to debug.
- Memory Hogs: Maintaining bidirectional frame buffers and tracking protocol state means every single WebSocket connection consumes significantly more server memory than an equivalent HTTP connection.
- No Native Reconnect: If a WebSocket connection drops (and it will), the browser does not care. You have to write all the custom logic to detect the drop, backoff, retry, and resynchronize state.
Why SSE is the Underdog You Need
SSE leans on the mature, battle-tested HTTP ecosystem. It doesn't require a protocol upgrade, it doesn't need a custom server, and it works flawlessly with standard load balancers.
- Native Auto-Reconnect: The
EventSourceAPI in the browser is brilliant. If the connection drops, the browser automatically attempts to reconnect on its own. It even sends aLast-Event-IDheader so your server knows exactly where to resume the stream. - Standard HTTP Routing: Because SSE is just a long-lived HTTP request, it scales like any other HTTP endpoint.
- Perfect for AI and Dashboards: If you are streaming an LLM response or pushing live price feeds to a dashboard, the client isn't sending data back through that channel (they just make a standard POST request to trigger the event). SSE perfectly models this server-push architecture.
Talk is Cheap. Look at the Code.
Here is how simple it is to implement SSE. No massive libraries, no custom protocols.
Backend (Node/Express):
app.get('/stream', (req, res) => {
// 1. Set the headers to keep the connection open
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 2. Push data whenever you want
const intervalId = setInterval(() => {
res.write(`data: ${JSON.stringify({ status: 'Processing...', time: Date.now() })}\n\n`);
}, 1000);
// 3. Clean up on disconnect
req.on('close', () => {
clearInterval(intervalId);
});
});
Frontend (Vanilla JS):
// The browser handles connection, streaming, and auto-reconnecting!
const source = new EventSource('/stream');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("New update:", data);
};
The Decision Framework
WebSockets and SSE aren't competitors; they solve different shapes of problems.
Choose WebSockets if:
- You are building a chat app, multiplayer game, or real-time collaborative canvas.
- The client needs to push data to the server at high frequencies (10+ times per second).
Choose SSE if:
- You are streaming AI responses, live notifications, news feeds, or financial tickers.
- The communication is primarily one-way (Server → Client).
- You want to avoid managing custom reconnections and complex load balancing.
Next time someone asks for real-time updates, don't immediately reach for the heaviest tool in the box. Give SSE a try.
Have you struggled with WebSocket scaling in production? Let's talk about it in the comments! 👇
Top comments (3)
The auto-reconnect advantage is real, but the backend snippet cannot deliver the resume half of it. The browser only sends
Last-Event-IDif the server emittedid:lines, and your handler writesdata:alone, soEventSourcereconnects to a stream with no idea where the client left off. I measured it just now on Chrome 152 against two local endpoints, one writing exactly yourres.writeline and one addingid: 42: the first logged nolast-event-idheader on the initial request and on all four reconnects, the second logged none on the initial request and42on every reconnect after it. That seam belongs in the comparison, because "no native reconnect" on the WebSocket side is the cost of writing detect-and-resync yourself, while SSE hands you the reconnect for free and still leaves the resync to you unless every event carries an id the server can replay from.Great catch, Vinh! You are completely right—I highlighted the Last-Event-ID feature in the article but entirely missed adding the id: line in the backend snippet to actually make it work. Thanks for taking the time to test it in Chrome and keeping the code honest. For anyone reading this thread, the server interval should look like this to enable true state resumption:
JavaScript ->
const eventId = Date.now();
res.write(
id: ${eventId}\ndata: ${JSON.stringify({ status: 'Processing...', time: eventId })}\n\n);I really appreciate the detailed technical breakdown!
The
id:line is necessary but it isn't the resumption, and I ran your patched shape here before saying so. Server emitsid: Date.now()and closes after three events, browser reconnects on its own:Last-Event-IDnow arrives on every reconnect (null on the first connection, then1788567120868, then1788567125483), and because the handler still starts its interval from the top, the client received step-1, step-2, step-3 again on each one. So the header became honest and the stream became duplicating. For a status ticker nobody notices that; for anything the client actually applies it is worse than not advertising resume at all.The other half is what the id is made of.
Date.now()stamps when the frame was written, so resuming from it means mapping a wall-clock time back onto a position in whatever you were streaming — you need a buffer to look into, and a plain sequence number is the thing you'd look it up with. And nothing in the snippet readsreq.headers['last-event-id']yet, so that line has to exist before any id value means something.