Skip to content
← All writing
August 1, 2026·6 min read

WebSockets vs SSE vs Polling: Realtime for Apps

Sooner or later every app grows a feature that needs to know about changes now: a chat thread, a live order status, a dashboard that updates itself, a progress bar fed from the server. "Just use WebSockets" is the reflexive answer — it's the one everyone has heard of — and it's often the wrong one, because it buys capabilities the feature doesn't need at an operational price you pay forever. There are three real options, and they differ less in what they can do than in what they cost you to run: polling, server-sent events, and WebSockets, in ascending order of both power and responsibility.

Polling: the option that isn't embarrassing

Plain polling — the client asks "anything new?" every N seconds — has a bad reputation it doesn't fully deserve, mostly inherited from an era before HTTP caching was used properly. It's stateless, works through every proxy, firewall, and corporate network on earth, needs zero special infrastructure, survives server restarts and deploys without a thought, and its failure mode is gloriously benign: "data is N seconds late." Every other transport in this post has failure modes involving the words "silently" and "stale connection."

Polling is right when updates are infrequent and latency tolerance is measured in tens of seconds: order status, notification badge counts, a leaderboard that refreshes each minute. Two refinements make it genuinely efficient. Pair it with ETags so the common "nothing changed" answer costs a 304 and a few header bytes rather than a full response. And back off adaptively — poll fast when the screen is visible and something is in flight, slowly or not at all when the app is backgrounded; the OS will punish you for ignoring this anyway.

Where polling genuinely fails is arithmetic: per-second freshness across many clients multiplies into torrents of requests for mostly-empty answers, and on mobile, each request is a radio wakeup that taxes the battery. The tell is behavioral: when you catch yourself shortening the interval to fake immediacy — 10 seconds, then 5, then 2 — the feature is asking for a push transport. Listen the first time.

SSE: push, without leaving HTTP

Server-sent events are the chronically under-used middle option. The client opens a normal HTTP request; the server keeps it open indefinitely and writes small text events down it as they happen. That's the whole trick. If it sounds familiar, it should — it's the same mechanism LLM APIs use to stream tokens, so if you've built a streaming AI feature, you already run SSE in production and have already met its quirks.

Because SSE is HTTP, it inherits the boring virtues that WebSockets give up: standard Authorization headers work unchanged, ordinary load balancers and proxies handle it (with one configuration note below), and — the underrated gift — reconnection is built into the protocol. A dropped client automatically reconnects and sends the Last-Event-ID header telling the server where it left off; assign IDs to your events and you get gap-free recovery nearly for free. With WebSockets you write all of that yourself.

The defining limitation: one direction. The server pushes; the client talks back via regular HTTP requests. Before treating that as a dealbreaker, look at your actual feature — for most "live" functionality (feeds, notifications, status updates, progress, dashboards, prices), the traffic is overwhelmingly asymmetric: a stream of updates flows down, and the user's occasional actions go up as perfectly ordinary POSTs. That's not a workaround; it's the natural shape of the interaction, and SSE matches it exactly.

Operationally, the caveats are the same as any long-lived HTTP response: buffering middleware must be told not to buffer (nginx's proxy_buffering off — the same fix as for LLM streaming), and idle streams need a keep-alive comment every 15–30 seconds so intermediary timeouts don't reap them.

WebSockets: full duplex, full responsibility

A WebSocket starts as an HTTP request and upgrades into a persistent two-way channel. Both sides can send at any time with minimal per-message overhead — the correct tool when the client also talks constantly and latency matters in both directions: chat with typing indicators and presence, collaborative editing, multiplayer game state, live location sharing.

The cost is that you leave plain HTTP behind, and a set of responsibilities quietly transfers from the protocol to you:

  • Reconnection and resumption — nothing is built in. You write the exponential-backoff retry, the connection-state machine, and the "what did I miss while disconnected" recovery — or carefully adopt a library that does, and learn its behavior anyway when it misbehaves.
  • Stateful servers — a connection pins each client to one machine, so scaling past a single server means shared pub/sub infrastructure (typically Valkey or a message broker) so a message arriving at server A reaches the client connected to server B. Deploys now have a story: every restart disconnects everyone.
  • Heartbeats — mobile networks and NATs kill idle connections silently; without application-level ping/pong you'll cheerfully send messages into the void and discover it from support tickets. ("Silently" and "stale connection," as promised.)
  • Auth wrinkles — the browser upgrade request can't carry custom headers, so you design token-in-query-string (mind your logs) or a first-message auth handshake, and you think about origin checks yourself.

None of this is exotic — chat products do it every day at vast scale — but it's real engineering and permanent operational surface. Buy it because the feature genuinely needs bidirectionality, not because WebSockets sound more serious than the alternatives. An architecture that says "SSE" is not junior; it's someone who read the requirements.

The decision rule

  • Updates infrequent, seconds of staleness fine → poll, with ETags and adaptive backoff.
  • Server pushes, client mostly listens → SSE.
  • Both sides chatty, low latency both ways → WebSockets, with the responsibilities budgeted honestly.

And they mix freely, per feature rather than per app: a delivery app might poll for order history, hold one SSE stream for live delivery status, and open a WebSocket only inside the support-chat screen — created when the screen appears, closed when it goes. The scarce resource on mobile is the persistent connection (battery, server memory, reconnect complexity); hold at most one, and only while it's earning its keep. What you should avoid is one WebSocket "because we might need it," carrying data three cheaper transports could have delivered.

The takeaway

"Realtime" is a product word, not an architecture — translate it before building: how fresh does this data need to be, which direction does it flow, and how many clients hold connections at once? Poll until it hurts (with ETags, it hurts later than you think), upgrade to SSE when the server needs to push (and note you may already run it for AI streaming), and reserve WebSockets for genuinely two-way conversations that justify their upkeep. The best transport is the dullest one that meets the freshness your feature actually requires — dull transports leave their drama out of your on-call rotation.

Further reading