You Might Not Need WebSockets: SSE, Polling, and When Each Actually Fits
A team I worked with spent two sprints building a WebSocket infrastructure, connection management, reconnection logic, a Redis pub-sub backplane to fan out messages across multiple server instances, for a feature that turned out to be a one-directional notification badge that updates when a background job finishes. That entire feature could have shipped in an afternoon with server-sent events and no infrastructure changes at all. This is not a knock on WebSockets, they are the right tool for genuinely bidirectional, low-latency use cases, but they get reached for reflexively far more often than the actual requirement calls for, and I want to walk through when each option actually fits.
The three options, briefly
Plain polling means the client asks the server on an interval, is there anything new, and the server responds immediately either way. Server-sent events, SSE, means the client opens a single long-lived HTTP connection and the server pushes messages down that connection whenever it has something new, but only in one direction, server to client. WebSockets give you a full bidirectional connection where either side can send messages at any time with minimal per-message overhead.
When plain polling is genuinely fine, and even preferable
Polling gets dismissed too quickly in a lot of technical discussions, but for anything where near-real-time, within a few seconds, is acceptable, and the feature is not expected to run for hours at a stretch, polling is simpler to implement, simpler to debug, and works through every proxy, firewall, and load balancer configuration without any special handling.
async function pollForUpdates() {
const res = await fetch('/api/notifications/latest?since=' + lastSeenId);
const data = await res.json();
if (data.length > 0) {
updateUI(data);
lastSeenId = data[data.length - 1].id;
}
}
setInterval(pollForUpdates, 5000);
The honest downside is server load if you have a huge number of clients polling frequently, since every poll is a full HTTP request-response cycle even when there is nothing new to report. For a feature with a few thousand active users polling every five seconds, this is genuinely not a problem on a modern server, and it is worth actually calculating your expected request volume before assuming polling will not scale, because I have seen teams prematurely reject polling based on a scaling fear that never gets checked against real numbers.
Server-sent events: the underused middle option
SSE solves the main weakness of polling, the server pushes updates the moment they happen rather than the client asking on a fixed interval, while staying dramatically simpler than WebSockets because it runs over plain HTTP, meaning no special proxy configuration, no separate protocol upgrade handshake, and automatic reconnection built into the browser's EventSource API with zero code required on your part.
app.get('/api/notifications/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const send = (data: object) => {
res.write('data: ' + JSON.stringify(data) + '\n\n');
};
const unsubscribe = notificationBus.subscribe(req.userId, send);
req.on('close', unsubscribe);
});
On the client, the entire integration is a handful of lines with no library needed:
const events = new EventSource('/api/notifications/stream');
events.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
That built-in reconnection behavior in EventSource is genuinely underrated, if the connection drops, the browser automatically retries with the last received event ID available for you to use for resuming from where you left off, all without you writing any reconnection logic yourself, which is a meaningful chunk of the complexity you would otherwise have to hand-build for a WebSocket-based approach.
The real limitation is the one-way constraint. SSE is server-to-client only, so any feature that needs the client to send frequent messages back, live collaborative cursors, a chat input with typing indicators, needs either a hybrid approach with regular POST requests for the client-to-server direction alongside SSE for server-to-client, or a genuine WebSocket connection if the back-and-forth is frequent and latency-sensitive enough to justify it.
When you actually need WebSockets
The honest cases where WebSockets earn their complexity: genuinely bidirectional, high-frequency interaction, live multiplayer cursor positions in a collaborative document editor, a multiplayer game's state synchronization, a chat application with typing indicators and read receipts flowing constantly in both directions. In these cases the overhead of establishing a new HTTP request for every client-to-server message, which SSE plus polling would require, becomes the actual bottleneck, and a persistent bidirectional connection is solving a real problem rather than a speculative one.
The operational cost is real too, and worth naming honestly. WebSocket connections are stateful and tied to a specific server instance, which means horizontally scaling a WebSocket-backed feature requires either sticky sessions at your load balancer or a pub-sub backplane, commonly Redis, to fan messages out across instances, exactly the kind of infrastructure that team I mentioned at the start built for a feature that never needed it. Reconnection logic, missed-message handling during a dropped connection, and connection limit management on your infrastructure are all things you own once you choose WebSockets, none of which are free.
The actual decision framework
I now default to asking two questions before reaching for WebSockets. Does the client genuinely need to send frequent messages, not just occasionally, but as a core part of the interaction. And is sub-second latency actually required by the feature, not just nice to have. If both answers are no, SSE covers the overwhelming majority of what people call real-time features, dashboards, notifications, live status updates, progress indicators, with a fraction of the operational complexity. If neither answer is yes and even a few seconds of staleness is acceptable, plain polling is often the right call, and resisting the urge to reach for something more sophisticated than the requirement actually calls for is, more often than people expect, the more senior engineering decision, not the less impressive one.
A hybrid worth mentioning that we ended up using for one specific feature: a polling endpoint that supports long-polling, where instead of returning immediately when there is nothing new, the server holds the connection open for up to thirty seconds waiting for something to report, and returns early the moment there is data. This gets you a meaningful chunk of SSE's responsiveness using plain request-response semantics that every piece of infrastructure already understands natively, no special content type, no persistent connection handling on the client beyond a normal fetch call, and it is a reasonable middle step if SSE feels like more infrastructure change than you want to take on for a first real-time feature. It is not free, holding connections open for up to thirty seconds at scale has its own resource considerations on the server side, but for a feature with moderate traffic it is a genuinely underused option that splits the difference nicely between plain polling and a persistent streaming connection.
Whatever you choose, the one habit I would recommend regardless of which mechanism you land on is instrumenting the actual latency your feature delivers end to end, not just assuming your chosen mechanism is fast enough because it sounded fast in the abstract. We measured actual time-from-event-to-UI-update across all three approaches on the same feature during our evaluation, and the gap between polling at a five-second interval and SSE was, on average, smaller than I expected going in, because most of the perceived lag in the polling version came from a slow database query on the polling endpoint itself, not from the polling interval. Fixing that query made plain polling feel nearly as responsive as SSE for that specific feature, which was a useful reminder that the transport mechanism is often not the actual bottleneck people assume it is, and measuring before optimizing applies just as much to real-time feature choices as it does to anything else in engineering.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.