DEV Community

Krishnam Murarka
Krishnam Murarka

Posted on

We Killed 400 Clients' 5-Second Polling Loop and Cut Server Load by 90%

Our status dashboard feature started life as the simplest thing that could work: every connected client polled a status endpoint every five seconds. It was easy to build, easy to reason about, and for the first year, with maybe 30 concurrent clients, it was completely invisible as a cost. By the time we had around 400 clients polling continuously, that endpoint was our single highest-traffic route by a wide margin, and most of those requests were doing nothing but confirming that nothing had changed.

The math is what finally forced the conversation. Four hundred clients polling every five seconds is 80 requests per second sustained, every second of every day, regardless of whether any status ever changed — and in practice, status changes were rare, maybe a few per minute across the whole client base. We were running a database query and serializing a response 80 times a second to deliver, on average, almost no new information. The server cost of that endpoint had become disproportionate to the actual amount of data flowing through it.

Moving to WebSocket push meant rethinking who initiates the update, not just changing the transport

The naive migration is "same data, pushed instead of pulled," but that undersells what actually changes: with polling, the client decides when to check. With push, the server decides when something is worth telling anyone about, which meant we had to go find every code path that mutated status and make sure it fired an event into our pub/sub layer at the moment of the change, not just leave that logic implicit in "the next poll will pick it up." That surfaced two places where status changes weren't clearly demarcated — cleanup jobs that updated records in bulk without a clear single point to hook an event into. We had to add that instrumentation before push was viable at all, which took longer than the WebSocket plumbing itself.

We kept a polling fallback rather than making WebSocket mandatory

Some of our clients sit behind corporate proxies that don't handle long-lived WebSocket connections well, and we didn't want the migration to trade one failure mode (server load) for another (silently broken clients behind a hostile proxy). Clients now attempt a WebSocket connection on load; if it fails or drops, they fall back to a much slower poll — every 60 seconds instead of 5 — as a safety net, not a primary channel. In practice under 3% of clients end up on the fallback path, but that 3% would have been a very bad debugging session if we hadn't planned for it.

Reconnection logic mattered more than we expected. A WebSocket connection dying — Wi-Fi hiccup, laptop sleep, load balancer cycling — is normal, constant, background noise at 400-client scale, and our first version handled reconnects by just re-establishing the socket and waiting for the next push. That has a real gap: any status change that happened during the disconnected window is silently missed, because push, unlike polling, doesn't naturally "catch up" a client that wasn't listening. We fixed this by having clients request a state snapshot immediately on reconnect, before resuming push, so a dropped connection costs at most one extra request instead of a permanently stale UI until the next unrelated change happens to arrive.

The load drop was almost exactly what the math predicted, which was oddly satisfying. Sustained request volume on the old polling endpoint dropped by about 90% once the majority of clients moved to push, with the remaining load being connection heartbeats (much cheaper than a full status query) plus that small fallback-polling cohort. Just as important as the raw server load number: status changes now reach clients within a second or two of happening instead of up to five seconds later, which for a monitoring dashboard is a meaningfully different product experience, not just a cost optimization.

The lesson that stuck with us wasn't really about WebSockets specifically — it was that polling intervals are a tax you pay whether or not anything happened, and that tax scales with client count in a way push architectures don't. Five seconds felt harmless when we picked it. It stopped being harmless somewhere around client number 200, and we didn't notice until the bill did.

Rethinking how a system delivers updates, rather than just how fast it delivers them, is a pattern we come back to a lot in the systems we build at Edilec — more on our approach at edilec.com.

Top comments (0)