Long polling vs WebSockets #

Long polling is the transport real-time systems used before WebSockets, and it has never gone away: Socket.IO still starts every connection with it, many enterprise integrations rely on it, and it is the fallback that works when a corporate proxy mangles Upgrade headers. The question comes up whenever a team inherits a long-polling system, builds for a locked-down network, or wonders whether WebSockets are worth the operational effort for a feature that updates once a minute. The answer depends less on raw latency — both are fast when idle — than on message rate, server resources per client, and how each behaves when networks and proxies misbehave.

Root cause #

Long polling emulates server push with ordinary HTTP. The client sends a request; the server holds it open until it has data or a timeout (typically 20–30 seconds) passes; it responds; the client immediately sends the next request. Each message costs a full HTTP request/response, headers and all, and there is a gap between one response and the next request during which the server cannot deliver anything.

WebSockets upgrade one HTTP request into a persistent, bidirectional channel. After the handshake, messages in either direction cost a few bytes of framing, and the server can send the instant data exists. The cost moves from per-message overhead to per-connection state: a WebSocket is a long-lived TCP connection that every hop on the path must support and keep open.

Neither is universally better. Long polling spends bandwidth and latency per message but survives any infrastructure that understands HTTP; WebSockets are dramatically more efficient per message but depend on every proxy, load balancer and firewall handling upgrades and long-lived connections correctly, the issue explored in WebSockets behind corporate proxies.

The same two messages over each transport Long polling needs a held request and a full response per message and a new request after each; a WebSocket upgrades once and then sends each message as a small frame. The same two messages over each transport Client Server LP: GET /poll (held open) LP: 200 message 1 LP: GET /poll again (new headers) LP: 200 message 2 WS: one upgrade, once WS: frame 1 (2–6 bytes overhead) WS: frame 2 Between a long-poll response and the next request, the server has nowhere to send
Long polling pays per message; WebSockets pay per connection.

Resolution #

Choose by workload, using the concrete numbers rather than intuition. The implementation below is a correct long-polling endpoint — worth having even in a WebSocket system, as a fallback — and it shows where the costs come from.

import express from 'express';

const POLL_TIMEOUT_MS = 25_000; // under typical 30 s proxy/browser idle limits
const app = express();

// Per-client cursor + waiters. In production this sits on Redis or a message log.
const log: { seq: number; data: unknown }[] = [];
const waiters = new Set<(items: typeof log) => void>();

export function publish(data: unknown) {
const item = { seq: (log.at(-1)?.seq ?? 0) + 1, data };
log.push(item);
if (log.length > 10_000) log.shift();
for (const w of waiters) w([item]); // wake every held request
waiters.clear();
}

app.get('/poll', (req, res) => {
const after = Number(req.query.after ?? 0);
// 1. Anything already newer than the client's cursor? Answer immediately.
const pending = log.filter((i) => i.seq > after);
if (pending.length) return res.json({ items: pending });

// 2. Otherwise hold the request until data arrives or the timeout passes.
const timer = setTimeout(() => { waiters.delete(wake); res.status(204).end(); }, POLL_TIMEOUT_MS);
const wake = (items: typeof log) => { clearTimeout(timer); res.json({ items }); };
waiters.add(wake);
req.on('close', () => { clearTimeout(timer); waiters.delete(wake); }); // client went away
});

app.listen(8080);

The client loops: request with its last seq, apply the items, request again immediately. The after cursor is essential; without it, messages published between one response and the next request are lost — the same gap problem as resuming a WebSocket, solved the same way as in resuming WebSocket sessions after reconnect.

Where the transports differ most is bandwidth at message rates above a few per minute. Each long-poll message carries request and response headers — often 500 to 800 bytes with cookies — while a WebSocket frame carries two to six bytes of framing.

Bytes on the wire per update: long polling vs WebSocket Wire bytes per update for payloads of 32, 128, 512, 2048 bytes: a WebSocket frame adds two to four bytes, an SSE event eight, and a polling round trip repeats 650 bytes of headers. Bytes on the wire per update: long polling vs WebSocket frame/protocol overhead only — polling assumes 650 B of HTTP headers WebSocket frame SSE event HTTP poll 0 B 1.0k B 2.0k B 3.0k B 32 B 128 B 512 B 2048 B
For small, frequent updates, headers dominate long polling's cost; the gap closes only for large payloads.
When each transport fits Long polling suits rare updates, hostile proxies and serverless hosting; WebSockets suit frequent updates, two-way messaging and low latency after bursts. When each transport fits Long polling WebSocket Updates per minute < 1 fine, simplest fine Updates per second > 1 header overhead efficient Client → server messages separate POSTs same channel Hostile proxies works anywhere may break Serverless / FaaS works, costs per hold needs managed gateway Latency after a message one RTT gap none Many systems use both: WebSocket first, long polling as the fallback
The deciding factors are message rate and the network path, not peak latency.

Edge cases #

Bursts. After each long-poll response, the next message waits for the client’s next request — at least one round trip. A burst of ten messages arriving 5 ms apart may be split across several responses, each adding a round trip. Batch everything available into each response, as the endpoint does, to limit the damage.

Server resources. Each held request occupies a connection and, on thread-per-request servers, a thread. On Node’s event loop, held requests are cheap, comparable to idle WebSockets; on traditional servers they are not. Serverless platforms bill for the time a function holds a request, which makes long polling expensive there.

Timeouts in the path. The hold timeout must be shorter than every idle timeout between client and server, or a proxy closes the request before the server answers, just as with WebSockets — the reasoning in tuning WebSocket idle timeouts across proxies applies directly.

Verification #

Measure both transports on your real path rather than trusting generic benchmarks. Instrument the client to record, for each message, the server’s publish timestamp and the time it was applied; compare the distributions for long polling and WebSocket under a representative message rate. Then check bandwidth in the Network panel over a minute of traffic: filter by the poll endpoint and sum transferred bytes, versus the WS connection’s frame sizes.

# Server side: how many requests does long polling cost per connected client per minute?
awk '$7 ~ /^\/poll/ {n++} END {print n " poll requests"}' /var/log/nginx/access.log

Divide by the number of connected clients and the minutes covered. For a feed with one update every few seconds, expect a dozen or more requests per client per minute — each one logged, authenticated and routed — versus zero additional requests for WebSocket clients.

Operational checklist #

FAQ #

Is long polling obsolete? #

No. It is less efficient per message, but it works through any infrastructure that handles HTTP and needs no special proxy configuration. It remains the right fallback, and a reasonable primary transport for low-frequency updates.

Why does Socket.IO start with long polling? #

To guarantee a working connection immediately, even on networks where WebSockets fail, and then upgrade to WebSocket in the background. The cost is extra requests and the need for sticky sessions during the handshake, covered in Socket.IO vs raw WebSockets.

How does long polling compare with Server-Sent Events? #

SSE keeps one HTTP response open and streams many events over it, so it avoids long polling’s per-message request overhead while still being plain HTTP. For one-way server push, SSE is usually the better HTTP-based choice; see when to use WebSockets over Server-Sent Events.

What hold timeout should I use? #

Somewhere between 20 and 30 seconds is typical — long enough to avoid excessive requests when idle, short enough to stay under common 30 and 60-second proxy limits. Measure the shortest idle limit on your path and stay well below it.

Back to WebSocket vs SSE vs WebRTC.