Real-TimeGENERALNETWORK-SPECIFICPLATFORM-SPECIFIC

Choosing a Real-Time Transport

Polling, long polling, SSE and WebSocket compared by direction, connection cost, infrastructure friction, reconnection, framing and what the browser already does for you.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Which transport does this feature actually need, and what does each one cost me after the prototype works?

The user intent

A person wants the number on the screen to be the current number. They do not want to press refresh, and they do not want to wonder whether what they are looking at is five minutes old.

The obvious build

Real-time means WebSocket. Open one socket when the app starts, push every update down it, and every screen becomes live for free. It is the modern option and it is what the tutorials use.

Why it breaks

The socket is open and idle for the ninety-nine percent of the session where nothing changes, holding a connection on the client, on every proxy in the path and on a server process that now has to remember which of ten thousand connections cares about which entity.

How it breaks in a real browser
  • The socket is open and idle for the ninety-nine percent of the session where nothing changes, holding a connection on the client, on every proxy in the path and on a server process that now has to remember which of ten thousand connections cares about which entity.
  • The first deploy proves the socket is not the feature — the reconnect is. Every client drops at the same instant, every client retries immediately, and the service that just came back up falls over again (Reconnect and Backoff).
  • A user opens the dashboard in four tabs. Four sockets, four subscriptions, four copies of the same event, and four independent reducers that now disagree (Auth Across Tabs).
  • The data being pushed changes twice an hour. A poll on a slow interval would have been correct, cacheable, retryable and debuggable by reading the network panel — all of which the socket gave up (Debugging the Network).
  • A corporate proxy between the user and the service does not pass the upgrade through. The feature works everywhere except at the customer who pays the most.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Polling is ordinary HTTP on a timer. Each poll is a complete request with its own cache validators, its own auth check and its own status code. Nothing is held open, so nothing can be lost — the next poll is the recovery (The Life of a Fetch).
  • Long polling is a request the server deliberately does not answer until something happens or a timeout expires. It is still request/response; the client immediately issues the next one. The connection is held open per waiting client, but the shape is HTTP all the way down.
  • Server-Sent Events is one HTTP response that never ends: Content-Type: text/event-stream, a body the server keeps appending to, and a browser-side parser that turns it into events. Reconnection, event ids and named events are part of the API rather than your code (Server-Sent Events).
  • WebSocket starts as an HTTP GET carrying an upgrade request. If the server answers 101, the same TCP connection stops speaking HTTP and starts carrying frames in both directions, with no request/response pairing at all (WebSockets in the UI).
  • All four deliver at least once at best. None of them gives you exactly-once, and none of them guarantees that two events about two different entities arrive in the order they were produced (Ordering and Duplicate Delivery).
  • All four break. The interesting differences are in what breaking looks like and who notices — a failed poll is a failed request you can see, a dead socket is silence that looks identical to "nothing happened" (Network Failures Only the Client Can See).

What this makes the browser do

And which of it is avoidable.

  • Every held-open connection occupies a socket, and on HTTP/1.1 it occupies one of the small number of connections the browser will open to a single origin — which is the trap that makes SSE fail in the fifth tab and not the first (Server-Sent Events).
  • Every inbound message is a task on the main thread: parse, dispatch, reduce, re-render. A stream that delivers a burst of hundreds of messages costs more in reconciliation than in bytes (Tasks: The Unit That Cannot Be Interrupted, What a Component Costs to Render).
  • Polling costs request overhead the browser can amortise — connection reuse, HTTP caching, conditional requests — and none of that machinery is available to a socket (Browser HTTP Caching).
  • A push transport that updates state on every message can trigger style, layout and paint far more often than the display can show them. Coalescing messages into one state commit per frame is usually the single biggest win (The Rendering Opportunity).
  • Avoidable work: subscribing screens that are not visible, keeping a stream open in a backgrounded tab, and re-rendering a list whose visible rows did not change (List Virtualization).

Four transports, priced honestly

Read the table by column and it looks like a capability ladder with WebSocket at the top. Read it by row and it is obvious that the ladder runs in the opposite direction for everything except direction: each step up removes something the browser was doing for you and hands you the job.

The row that decides most real arguments is the second one. A held-open connection is not free at rest — it is a resource on the client, on every intermediary, and on a server that must now be stateful about who is listening to what (Stateless vs Stateful Services in Architecture).

  • Nothing in the table says "best". It says what each one stops doing for you (Polling vs Long Polling vs SSE vs WebSockets in Networking has the protocol-level view).
  • Rows three and seven are where production surprises live, and neither is visible on a developer machine.
  • The last row is the real price list: capability is bought with responsibility, and the responsibility is the next five lessons (WebSockets in the UI).
AspectPollingLong pollingSSEWebSocket
DirectionClient asks, server answersClient asks, server answers when there is newsServer to client onlyBoth directions, unpaired
Cost while idleNothing held open between pollsOne request parked per client, re-issued after each messageOne response held open per client for the sessionOne connection held open per client for the session
Infrastructure frictionNone — ordinary GETs through every proxy, CDN and logRequest timeouts in proxies and gateways cut the wait shortBuffering intermediaries can hold the body and deliver nothingNeeds upgrade support end to end; some proxies refuse it
ReconnectionNothing to reconnect — the next poll is the recoveryThe re-request is the loop; failure is a failed requestBuilt into EventSource, with a server-settable delayYours to write, including the backoff and the jitter
Message framingWhole response body, any content typeWhole response body per wake-upUTF-8 text events with optional event and id fieldsText or binary frames; the schema is entirely yours
Resuming after a gapThe next poll returns current state; the gap heals itselfSame, if the endpoint returns state rather than a deltaLast-Event-ID replays from a cursor if the server implements itNothing at all unless you design it (Resynchronisation After a Gap)
AuthChecked per request, like everything elseChecked per requestCookies on the request; no custom headers availableCookies on the handshake; no custom headers available
What the browser does for youCaching, conditional requests, status codes, retry policy you ownNothing beyond ordinary HTTPReconnect, event parsing, id tracking, named event typesFrame parsing and a close code. That is the complete list.

Polling is not the embarrassing option

SIMPLIFIEDThe comparison assumes a polling endpoint that returns current state rather than a delta since a cursor; a delta-polling endpoint has the same gap problem as a push transport, because it can be asked to replay from a position the server no longer retains.

The case against polling is usually stated as waste: requests that return nothing new. The case for it is that a poll carries an entire snapshot of the truth, so a client that missed ten of them is not behind — it is exactly one poll away from correct. That property is worth a great deal, and it is precisely the property a push transport gives up.

The timeline below is schematic, in relative units, and its point is the shape rather than any number. The polling client is stale for at most one interval and needs no recovery path. The pushing client is fresher while the connection is up and arbitrarily wrong while it is down, which is the trade the diagram is actually about.

The same three changes, two transportsrelative units — a shape, not a measurement
Change A on the server
Poll (returns nothing new)
Poll (delivers change A)
Push delivers change A
Connection drops
Changes B and C occur
Poll (delivers B and C together)
Reconnect + resync
  • Change A on the serverThe moment the truth changed. Neither client knows yet.
  • Poll (returns nothing new)The request people call waste. It is also the health check, the auth check and the cache revalidation.
  • Poll (delivers change A)Staleness is bounded by the interval, and the bound holds even if every previous poll failed.
  • Push delivers change AFresher, by roughly the part of the interval the poll had left to wait.
  • Connection dropsThe push client now shows confidently stale data and has no idea. The poll client cannot tell the difference between this and quiet.
  • Changes B and C occurMissed entirely by the push client unless a replay cursor exists.
  • Poll (delivers B and C together)The gap healed itself with no code. This is polling's whole argument.
  • Reconnect + resyncThe push client must reconnect, then decide what it missed and how to close the gap (Resynchronisation After a Gap).

The push client is fresher in the good case and needs three additional subsystems — reconnect, ordering, resync — to be merely correct in the bad one.

Choosing, out loud

The decision is not "which is best" but "what is the weakest thing that meets the freshness requirement, given the direction of the data and the number of connections we are willing to hold open". Write the freshness requirement down before the argument starts; most of the disagreement is people assuming different answers to it.

Note the first option. A surprising number of "live" requirements are satisfied by refetching when the tab becomes visible again, because the user was not looking at the screen while it was stale (State Synchronization).

What should carry these updates?

How fresh must this be, in which direction does the data flow, and how many connections are you prepared to hold open?

No live transport — refetch on focus and after mutations

when The user only acts on the data when they are looking at it, and their own actions cause most of the changes they care about.

cost Stale between visits, and someone will eventually ask why two people see different numbers at the same moment (Query Keys and Invalidation).

Polling

when Changes are infrequent or bounded staleness is acceptable; the endpoint can return current state; you want HTTP caching, per-request auth and a readable network panel.

cost A constant floor of background requests that scales with users, and staleness bounded by the interval rather than by the event.

Long polling

when You need near-immediate one-way delivery but cannot rely on SSE or WebSocket surviving the network path, and the message rate is low.

cost A parked request per client with proxy timeouts to tune, plus a re-request per message. Rarely the right answer now that SSE exists, but it survives hostile intermediaries.

Server-Sent Events

when One-way, text, moderate volume — notifications, activity feeds, job progress, streamed model output (Streaming a Response Without Melting the Device).

cost One held-open connection per tab, which is a real constraint on HTTP/1.1; no binary; no client-to-server channel, so anything the client sends is a separate request (Server-Sent Events).

WebSocket

when The client sends nearly as much as it receives, latency in both directions matters, or the payload is binary — collaborative editing, presence, live cursors, interactive sessions.

cost You own reconnect, backoff, heartbeats, auth refresh, message schema, versioning and backpressure, forever (WebSockets in the UI).

How to build it

Most important first.

  • Start by naming the required freshness in words a product person would use: "within a second", "before the user acts on it", "by the next time they look". Most answers are not "instantly", and the ones that are usually concern a small slice of the screen.
  • Prefer the weakest transport that meets that requirement. Polling on a sensible interval, plus a refetch when the tab regains focus, covers a genuinely large share of "live" features and keeps every HTTP affordance (Stale-While-Revalidate).
  • Choose SSE when the direction is one-way and the payload is text. You get reconnection, event ids and resumption without writing any of it, and it stays HTTP, so every proxy, log and auth check in the path still works (Server-Sent Events).
  • Choose a WebSocket when the client genuinely sends as much as it receives — collaborative editing, presence, live cursors, an interactive session — and accept that you have signed up to own reconnection, heartbeats, schema and versioning (WebSockets in the UI).
  • Whatever you choose, design the resynchronisation first. A transport is only as good as its answer to "I was gone for ninety seconds; what did I miss?" (Resynchronisation After a Gap).
  • Keep one connection per tab at most, owned by one module, with screens subscribing to it rather than opening their own. Two sockets in one page is a state-ownership problem wearing a networking costume (Who Owns This State?).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Live updates are an accessibility decision before they are a transport decision. Content that changes without a user action can be announced, silently changed, or announced so often that a screen reader becomes unusable — and the transport does not decide which (Live Regions and Announcement).
  • The higher the update rate, the stronger the case for *not* announcing each one. Batch into a periodic summary — "3 new orders" — rather than reading every arrival aloud.
  • Anything that reorders or removes content underneath a keyboard or screen-reader user takes their place away. A list that re-sorts on every push is a list nobody can read from top to bottom (Focus Management).
  • Polling has a quiet accessibility advantage: updates arrive at moments you chose, so you can align them with a natural pause rather than interrupting mid-sentence.

What can go wrong

Failure modes
  • A socket that is open but dead. TCP has no idea the path is gone, the browser reports nothing, and the UI shows confidently stale data until the user reloads (Reconnect and Backoff).
  • A polling interval that was fine for a hundred users and is a self-inflicted denial of service at a hundred thousand, because every client polls on a schedule anchored to page load and the load arrives in waves.
  • A buffering proxy that holds an SSE or long-poll response until it is "complete". The feature works locally and delivers nothing through the customer's network appliance.
  • The mitigation failing: adding a heartbeat to detect dead sockets, and then treating a missed heartbeat as a reason to reconnect immediately, which converts one flaky network into a reconnect loop (Retries, and the Duplicate Order).
  • Choosing a transport before knowing the fan-out shape. "Push every change to every client" is a server architecture decision the frontend cannot rescue (How API Shape Drives UI Complexity).
What can arrive out of order
  • A poll and a push can both be in flight. If a feature has an initial fetch plus a live stream, the snapshot and the first events race, and applying them in arrival order can resurrect a deleted row (Out-of-Order Responses).
  • A user action and an inbound event race constantly: the user clicks delete while the server pushes an update for the same entity. Whichever arrives second wins unless the merge is written deliberately (Optimistic UI).
  • Across tabs, four connections receive the same event at four slightly different moments, and each tab's local state diverges for as long as that skew lasts (State Synchronization).
Security
  • Polling and long polling are ordinary requests, so every existing control applies unchanged: cookies with their attributes, auth checks per request, rate limits, CSRF considerations for anything that mutates (Cross-Site Request Forgery).
  • A held-open connection moves the authorization check to connection time. Whatever the user was allowed to see when they connected is what the stream keeps sending unless the server re-checks — permissions revoked mid-session do not revoke the stream (Authorization-Aware UI).
  • A WebSocket handshake is not subject to the same-origin restrictions that govern fetch. The browser will send it cross-origin with the page's cookies attached and no preflight, so validating the Origin header on the handshake is the server's job and nobody else's (The Same-Origin Policy).
  • Content-Security-Policy governs which endpoints a page may connect to via connect-src, and it covers fetch, EventSource and WebSocket alike. It is a useful containment control and not an authentication one (Content Security Policy).
  • Anything the stream pushes is untrusted input that will end up in the DOM. A pushed message is exactly as dangerous as a fetched response, and it arrives when nobody is looking at the screen (Cross-Site Scripting).
Misreads
  • "WebSocket is the modern one, so it is the default." It is the one that hands you the most work. Modern is not the axis; direction, volume and freshness are.
  • "Polling is wasteful." A poll on a slow interval to a well-cached endpoint can be cheaper end to end than an idle socket per user, and is enormously cheaper to operate (Retries, and the Duplicate Order).
  • "SSE is just long polling with a nicer name." Long polling ends after each message and re-requests; SSE is one response the server appends to, with framing, ids and reconnection defined by the browser (Server-Sent Events).
  • "Once the socket is connected, delivery is guaranteed." The socket guarantees ordering *within one connection* and nothing at all across a reconnect (Ordering and Duplicate Delivery).
  • "We can decide the transport later, it is just plumbing." The transport determines whether missed messages are possible, which determines whether your state model needs a resync path at all (Resynchronisation After a Gap).

Measuring it, and what changes in the field

How you would see this
  • The Network panel distinguishes the four immediately: repeated identical requests, one long-pending request, one request with an eventsource type and a Messages/EventStream tab, or a 101 with a frame list (Debugging the Network).
  • Message count and message rate per session, from your own instrumentation. Nobody discovers "we push 4,000 messages to an idle dashboard" from a screenshot.
  • Main-thread time attributed to message handling in the Performance panel — the honest cost of a push transport is rarely the bytes (The Real Cost of JavaScript).
  • Reconnect counts and disconnect reasons from real users. Local development has a zero percent disconnect rate, which is why this is field data or nothing (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a mobile network, connections are dropped constantly — by radio state changes, by handovers, by NAT timeouts. A transport that treats a disconnect as exceptional will be in its exception path most of the session (Reconnect and Backoff).
  • On HTTP/1.1 the per-origin connection cap makes any held-open transport expensive in a multi-tab session; on HTTP/2 and HTTP/3 the stream is multiplexed onto one connection and the constraint largely disappears (Server-Sent Events).
  • In a backgrounded tab, timers are throttled hard, so a polling interval becomes an approximate suggestion — and a socket may stay open doing nothing useful for hours (Long-Lived Clients and Version Skew).
  • With a large dataset, push volume scales with what changed rather than what the user can see, so the cheapest optimisation is usually subscribing to less rather than transporting it faster.
What this costs
  • Polling trades a fixed background load and bounded staleness for total simplicity: no reconnection code, no ordering code, no resync code, and every request visible in the network panel.
  • SSE trades bidirectionality and binary payloads for reconnection and resumption you do not have to write.
  • A WebSocket trades every HTTP affordance — caching, conditional requests, per-request auth, per-URL observability, ordinary proxies — for genuine bidirectional messaging.
  • Choosing the strongest transport "so we never have to change it later" front-loads all four of this module's hard problems onto a feature that may never have needed any of them.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe four transports and their direction, framing and reconnection semantics are defined by the HTML and WebSocket specifications, so the model holds across Blink, Gecko and WebKit; only connection accounting and devtools presentation differ between them.
  • NETWORK-SPECIFICThe cost of holding a connection open depends on the protocol version: on HTTP/1.1 each held-open response consumes one of a small per-origin connection budget, while on HTTP/2 and HTTP/3 it is one multiplexed stream among many on a single connection, which changes the advice for SSE completely.
  • PLATFORM-SPECIFICIntermediaries decide much of this: corporate proxies and some load balancers buffer streaming responses or refuse the WebSocket upgrade, so a transport that works on every browser can still fail for one customer's network and not another's.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Distributed Systems — fan-out, subscription routing and which node holds the connection for which user are server-side problems the client can only make worse or better, never solve.