"Before SSE and WebSockets, the only way to get server-pushed updates was to ask repeatedly. Naive polling is wasteful; long-polling fixes the wastefulness; both still ship in production today for cases where the higher-tech options don't fit."
Naive Polling — Ask Repeatedly, Throw Most Answers Away
Client asks the server "any updates?" every N seconds. If nothing changed, the server returns an empty result; client waits and asks again. Simple to implement; works with any HTTP server; survives every proxy in the world.
Costs:
- Latency. Average time-to-deliver = N/2. Polling every 5s means user-visible latency between 0 and 5s, average 2.5s.
- Bandwidth. Empty responses still cost an HTTP roundtrip (request line + headers + 200 OK + empty body = ~500 bytes both ways). At 5s intervals across 10,000 clients, that's 1 MB/s of nothing.
- Server load. Even idle clients hit the endpoint constantly.
When it's fine: low-stakes status checks (build progress every 10s), background workers, anywhere latency tolerance is high.
Long-Polling — Server Holds the Request
Same pattern, smarter execution: client asks; server doesn't respond immediately if nothing's available. Instead, the server holds the connection open, waiting for an event. When something happens (or a server-side timeout fires, ~30s), the server responds with the event (or empty + 'timeout' marker). The client immediately starts a new long-poll.
Wins over naive polling:
- Near-zero latency. The moment data is available, server pushes it. Client receives it within one RTT.
- Lower request rate. One request per event (or per ~30s timeout) instead of one request per 5s.
Costs:
- One persistent connection per pending client. Server holds N file descriptors for N waiting clients. Async/await frameworks (FastAPI, Node, Go) handle this cheaply; thread-per-request servers (older PHP, Apache prefork) do not.
- Proxy/timeout interference. Some intermediaries close connections after ~60s of silence. Server-side timeout must come before any intermediary's.
The Async Cost Inversion
In a synchronous (thread-per-request) server, holding 10,000 long-poll connections requires 10,000 threads — typically infeasible. In an async server (Python's asyncio, Node.js, Go goroutines), 10,000 pending connections cost almost nothing — each is a stalled coroutine, not a thread. The framework's concurrency model determines whether long-polling is practical at scale.
When Long-Polling Wins vs SSE / WebSocket
Long-polling beats SSE when:
- Clients are behind firewalls that drop long-held connections (some corporate environments) — but in that case SSE has the same problem.
- You need request/response semantics for each event (the client wants to acknowledge each delivery). SSE is one-way push only.
Long-polling loses to SSE/WebSocket when:
- You expect high event frequency. Each event re-opens the long-poll — that's per-event HTTP overhead SSE doesn't have.
- You want true streaming (server pushes a continuous flow of small events). SSE was designed for this.
- You need bidirectional traffic. Both directions need to flow real-time → WebSocket.