Skip to content
ALI HAITHAM·TECH·responds in <1h
  • Homestart here
  • Workcase studies, projects
  • Serviceswhat I will build for you
  • Writingessays, series
  • Traincourses, labs, cohorts
  • Aboutthe engineer behind this site
Sign inStart a project
ALI HAITHAM · TECH
  • Home↗
  • Work↗
  • Services↗
  • Writing↗
  • Train↗
  • About↗
Sign inStart a project
Online
ALI HAITHAM·TECH

Engineering studio. Damascus, GMT+3.

aliyosef.online

Studio

  • Work
  • Writing
  • Training
  • About
  • Contact me

Portal

  • Sign in
  • Open a ticket
  • Track project
  • My account

Resources

  • Docs
  • Status
  • Changelog
  • Brand kit
  • Privacy
  • Terms

Newsletter

Field notes and tech news. Weekly. No fluff.

Free. Unsubscribe anytime.

Find me elsewhere
© 2026 Ali Haitham Yosef. All rights reserved.Hand-built in React 19. No frameworks of frameworks.Last deployed · 2026-05-08All systems operational
COURSESync engines, end-to-end· in lesson 01.02↗
  1. Home/
  2. Writing/
  3. Sync engines
Sync engines

When polling beats websockets

A year of trade-offs in Mohaseb. Why the obvious answer was wrong on our network conditions.

May 5, 2026·8 min·by Ali Yosef

Loading essay…

ShareXLinkedInRSS
Get the next essay in your inbox.New posts every Sunday morning. Engineering, in plain language. Free, unsubscribe anytime.Subscribe→
AY
Ali Haitham Yosef

Engineer and writer. Builder of multi-tenant platforms, sync engines, and quietly reliable systems. 600+ shipped projects since 2015.

SubscribeStart a project→

Read next

  • CR
    Apr 28, 2026·Sync engines

    Conflict resolution in offline-first apps

    Last-write-wins is almost always wrong. Here is what works in practice when two clients disagree.

    10 min
  • SE
    Series
    Apr 20, 2026·Sync engines

    Building a sync engine, part one: the queue

    Series opener. The shape of an outgoing queue, why ordering matters, and the test harness that catches everything.

    14 min
  • RS
    Apr 12, 2026·Sync engines

    The reconnect storm problem

    When 800 phones come back online at once, what happens to your server. The patterns that survive.

    7 min
←Back to all essays

For most of 2024 we were certain we needed websockets. The product was a multi-tenant accounting platform with a Flutter app on the client and a Laravel backend; data had to feel live across devices, and the textbook answer is "open a websocket and push." So we did. Six months in, we ripped it out, replaced it with disciplined polling, and the user-facing latency got better. Here is what we learned, and when polling actually wins.

The textbook answer is right, on paper#

The argument for websockets is well-rehearsed and not wrong:

  • One persistent connection, no per-request handshake overhead.
  • Server can push the moment a change happens.
  • Lower aggregate bytes on the wire over a long session.

If you are building a chat app, a multiplayer cursor, or a trading view, those properties are not negotiable. You need them. Stop reading.

What changed when we left the lab#

The accounting app is used in shops, warehouses, restaurants, and offices across rural Saudi Arabia, Syria, and the Gulf. Network conditions there are not the conditions on your laptop in a cafe. Specifically:

  1. Most cellular sessions are not persistent. Devices switch towers every few minutes; carrier-grade NAT timeouts are aggressive; some networks reset idle TCP connections after sixty seconds.
  2. WiFi access points reboot. Routers reboot. Phones go to sleep with the screen off and the radio cold. Reconnect logic has to be perfect or the user notices nothing arrived.
  3. Many devices are old. An Android 9 phone with 2 GB of RAM and a browser webview cannot afford a websocket worker plus the rest of your runtime. Memory pressure kills the connection silently.
The blunt observation

Websockets assume the network has a memory. On real-world consumer networks, the network has dementia.

The reconnect storm#

Our breaking point was a Tuesday morning. Power had been out overnight across a city we had several hundred users in. When the power came back, all of those phones came back online at the same minute. They all tried to reopen their websockets at the same minute. The Reverb server fell over. The users got an empty screen. We got a phone call.

You can engineer around the storm. Jittered backoff, server-side rate limits, a connection pool, a separate gateway. We did all of that. It helped. It did not feel reliable.

Why polling started winning#

Polling has a property that is easy to overlook because we are trained to think of it as primitive: every request is independent. There is no shared state on the wire. There is no "is the connection still healthy?" question to answer. The client asks; the server answers; we move on.

This shape is forgiving in ways a websocket is not:

  • A failed poll just retries on the next interval. No "broken pipe" state to recover.
  • A server restart drops zero connections, because there are no connections to drop.
  • HTTP caching, CDN edges, conditional requests with If-Modified-Since all become available again. Websockets opt out of that infrastructure.
  • Observability is trivial. Every poll is a normal HTTP request line in the log. No special tooling.

What "disciplined polling" actually means#

We did not just change the URL. The polling layer that replaced our websockets has a few properties that took iterations to get right:

interface PollConfig {
  intervalActiveMs: 8_000;     // user is interacting
  intervalIdleMs:   45_000;    // tab visible, no input recently
  intervalHiddenMs: 0;         // tab is hidden, do nothing
  jitterRatio:      0.2;       // ±20% so a fleet does not lockstep
  maxConcurrent:    1;         // never overlap polls
  cursor:           'updated_at';
}

A few things worth pulling out:

Visibility-aware#

When the tab is hidden, we do not poll. When it becomes visible again, we poll once immediately, then resume the interval. This single change cut our backend traffic by 60 percent and made the perceived latency better, because the foreground tab always polls within milliseconds of becoming visible.

Cursor-based, not full-fetch#

Every poll sends the highest updated_at it has seen, and the server returns only the rows newer than that. The response is almost always small. Most polls return zero rows.

One in flight at a time#

We collapse overlapping polls into one. If a poll is in flight when the next interval fires, we skip the next one and let the in-flight one finish. This sounds obvious; it is the source of half the bugs you will see in poll loops written in a hurry.

After two weeks of disciplined polling, the average user-perceived latency for "your transaction shows up on the second device" was 4.6 seconds. With websockets it had been 11 seconds, when it worked. The worse-on-paper protocol won on the metric that mattered.

Engineering log, week 23

When you actually still want websockets#

Polling is not the answer for everything. The cases where we kept push:

  • Notifications. Single, infrequent, high-importance events where a delay of even ten seconds is bad. We use a separate Reverb channel with a thin payload, and we treat the websocket as best-effort: if it is down, polling notices the same row in due course.
  • Live dashboards. Operations dashboards where the operator stares at the screen for hours. The session is long, the load is small, and the persistent connection pays for itself.
  • Concurrent editing. If two people are typing into the same form, you need push. Polling cannot serve a thirty-millisecond cursor position.

Outside those, default to polling.

The line we draw now#

If your users are on the same network as your server, and the session is long, and you are paying for low latency on every change, websockets are correct.

If any of those conditions break, polling, done with discipline, is the answer that survives contact with the real world.

That is not the architectural conclusion the textbook wants. It is the one that holds up at 8 AM on a Tuesday after a power outage.


The next essay in this thread is about how we structured the outgoing queue on the client so that polling-based sync stays correct under network flap. If sync engines interest you, the series page picks up where this one ends.