Near-real-time updates via long polling
Open in the interactive app: practice, quiz, mock interview →In one minute
- Long polling is server push built from ordinary HTTP: the client asks "anything new after X?", and the server holds the request open until there is something to say or a timeout passes.
- When the response arrives, the client immediately asks again with an updated cursor, so there is almost always one request waiting at the server.
- Latency is close to a real push channel, and it works through every proxy, firewall and load balancer that speaks HTTP.
- The hard parts are not the waiting: they are the cursor that prevents gaps and duplicates, timeouts that fit under every proxy's idle limit, and reconnect storms.
- Choose it when updates are occasional, clients are diverse or behind restrictive networks, or the consumer is a server (SQS, Consul, Telegram bots). Prefer SSE or WebSockets for high-rate streams.
How it works
From polling to long polling
Plain polling asks every few seconds whether anything changed. With a 5-second interval the average delay is 2.5 seconds, and most requests come back empty: 10,000 clients make 2,000 requests per second to learn, usually, that nothing happened.
Long polling moves the waiting to the server. The client sends GET /events?cursor=118. If event 119 already exists, the server answers at once. If not, it parks the request and answers the moment event 119 is published, or after, say, 25 seconds with an empty response. Either way the client sends the next request straight away. Delivery delay drops to about one network round trip, and empty responses drop to one per client per timeout period.
The cursor is the protocol
Between one response and the next request there is a gap of a round trip or more, and events published in that gap must not be lost. So the client never asks "what is new?" but "what is after the last thing I saw?". The server keeps a short, ordered log per channel and answers from it. A response carries the events plus the new cursor; a retry with the same cursor gets the same answer, so duplicates are harmless and nothing falls through the gap.
The cursor also bounds what the server must keep. If a client returns with a cursor older than the retained log, the server says so explicitly (etcd returns a "compacted" error; the Kubernetes API answers 410 Gone and the client must re-list (Kubernetes docs)) and the client reloads full state. That is the ordering and duplicates problem shape: the same cursor-and-log design appears in Kafka consumer offsets and in database replication slots.
Where you meet it
Long polling is common between servers, where it is often the best tool. SQS's ReceiveMessage with WaitTimeSeconds=20 (the maximum) is long polling, which is why idle consumers cost almost nothing; note that short polling is the default, so a consumer that never sets the wait time pays for a stream of empty receives (SQS docs). Consul's blocking queries pass ?index=<X-Consul-Index>&wait=5m and return when the watched data's index moves past it; the wait defaults to 5 minutes and is capped at 10 (Consul docs). Telegram's getUpdates takes offset and timeout. CI runners long-poll for jobs. In browsers, it was the core of Comet-era chat, and Socket.IO still opens every connection on HTTP long polling by default, then tries to upgrade to a WebSocket, so a blocked upgrade quietly leaves the client on polling (Socket.IO docs).
The same shape, other transports
Server-Sent Events keep one response open and stream many events through it; WebSockets upgrade the connection to a two-way message channel. Both avoid a request per event. Long polling pays a full request and response per batch, but needs nothing from the network except plain HTTP request-response, and each request is independent, so it can land on any server.
Down the stack
Layer 1The contract
A long-poll endpoint promises: given cursor c, return every event with sequence greater than c, in order, up to a batch limit, as soon as at least one exists; if none arrives within the wait time, return an empty result with the cursor unchanged. If c is older than the retained history, return a distinct error, never a silent partial answer. Delivery is at-least-once from the client's point of view, since a response lost in transit is simply re-fetched with the old cursor; idempotence comes from the cursor, not from the transport. Worst-case latency for an event is one round trip if a request is parked, and up to about two round trips if it lands in the gap between requests.
Layer 2The mechanism
The server keeps, per channel, an append-only buffer of (seq, payload) and a list of parked waiters (client, cursor, deadline). A request checks the buffer first, which is the step that closes the gap; only if nothing is newer does it park. A publish appends to the buffer and completes every waiter on that channel. A timer wheel or a min-heap keyed by deadline completes waiters that time out, with 200 and an empty list or 204 No Content.
Real systems differ in the details. Consul returns the index in the X-Consul-Index header and adds up to wait/16 of random jitter to each wait, so watchers do not all wake together. It also tells clients to reset to 0 if the index ever goes backwards. Since Consul 1.10 an optional streaming backend can replace long polling for some queries, the same move Kubernetes made. SQS checks all its storage hosts during a long poll rather than the weighted random sample a short poll reads, which is why it also returns fewer false empties (SQS docs). Kubernetes started with this pattern and evolved into a streaming watch keyed by resourceVersion, one response that never ends. RFC 6202 (2011) records the classic browser problems (RFC 6202). The old HTTP/1.1 advice of two connections per server had just been relaxed to 6 or 8 in browsers, and a parked request permanently uses one of them, shared across every tab on that origin. Caching must be suppressed with Cache-Control: no-cache (today no-store is the stricter choice). Over HTTP/2 the parked request is just one stream on a shared connection, so the per-host limit mostly stops mattering.
When the server is a fleet, a publish on node A must wake a waiter parked on node B. The usual answer is a pub/sub backbone (Redis pub/sub, NATS, Kafka) that every node subscribes to for the channels its parked clients care about, the same fan-out design used for WebSocket gateways.
Layer 3The machine
A parked request costs an open socket, its kernel buffers (a few KB while idle), the TLS session state (tens of KB in many libraries) and the server's own bookkeeping: a goroutine with a 2 to 8 KB stack in Go, a small promise and closure in Node, but a whole 1 MB-stack thread in a thread-per-request server, which is why long polling killed many early Java and PHP deployments. With epoll, 100,000 parked requests cost the CPU nothing until an event or deadline arrives. Each cycle re-sends HTTP headers, often 500 to 1,000 bytes in each direction, unless HTTP/2 HPACK compresses repeats.
Timeouts are set by the network, not the application. AWS ALB closes idle connections after 60 seconds by default (ALB docs), nginx's proxy_read_timeout is 60 seconds (nginx docs), and many corporate proxies and mobile carrier NATs drop idle flows after 30 to 120 seconds without telling anyone. A 25 to 30 second wait stays safely under all of them; RFC 6202 reports experiments with 120 seconds but calls 30 the safer value. On phones every completed poll can wake the cellular radio, which then stays in a high-power state for several seconds, so long waits are kinder to batteries.
Why it's built this way
- Plain HTTP is the one thing every network path supports, so push is emulated with requests the server answers late.
- Requests can be lost and there is a gap between them, so the client names its position with a cursor and the server keeps a short log.
- Intermediaries kill idle connections at 60 seconds or less, so the server answers empty well before that and the client re-asks.
- Many clients wake at once after an outage or a broadcast, so waits and reconnects get jitter and backoff.
- Each event costs a full HTTP exchange, so at high event rates the cost argues for a streaming transport instead.
Diagram
sequenceDiagram participant C as Client participant S as Server participant P as Publisher C->>S: GET /events?cursor=118 S->>S: nothing after 118, park (deadline 25s) P->>S: publish event 119 S-->>C: 200 [119], cursor=119 C->>S: GET /events?cursor=119 P->>S: publish event 120 (already buffered) S-->>C: 200 [120], cursor=120 C->>S: GET /events?cursor=120 S->>S: park S-->>C: 200 [] after 25s, cursor=120 C->>S: GET /events?cursor=120
Worked example
A logistics dashboard shows live delivery status to 80,000 concurrent dispatchers. Each dispatcher's channel sees about one update per minute; a wait time of 25 seconds is used.
Plain polling every 5 seconds: 80,000 / 5 = 16,000 requests per second, of which about 98% are empty, and an average delay of 2.5 seconds.
Long polling: each client completes a request when an update arrives (1 per 60 seconds) or when the wait expires. With updates arriving on average every 60 seconds, about 2 of every 3 cycles end in a timeout, so each client makes roughly 3 requests per minute: 80,000 x 3 / 60 = 4,000 requests per second, and updates arrive within one round trip, about 80 ms. At about 1.5 KB of headers per exchange that is 6 MB/s, against 24 MB/s for plain polling.
Memory: 80,000 parked requests across 8 Go servers is 10,000 each; at roughly 40 KB per connection including TLS and buffers, that is 400 MB per server. Fine.
The failure case: a deploy restarts one server, dropping 10,000 connections at once. If clients reconnect immediately, the remaining 7 servers take 10,000 new TLS handshakes in under a second on top of normal load. Clients that wait a random 0 to 5 seconds first spread that to about 2,000 per second, and the cursor guarantees nobody misses an update while they wait.
In the wild
- Roblox, 2021: a newer push mechanism under load. Roblox was down for 73 hours from 28 October. One root cause was Consul's streaming feature, which Roblox had enabled in place of long-polling blocking queries. Under very high read and write load on 128-core NUMA servers, streaming funnelled updates through a single Go channel, contention blocked KV writes, and median write latency rose from 300 ms to about 2 seconds. Disabling streaming fleet-wide on 30 October brought write latency back to 300 ms (Roblox).
- Kubernetes, 2024: re-lists that knock over the API server. When watches break, informers fall back to a full LIST, and the API server builds each LIST response in memory before sending a byte. In a test with 400 one-megabyte Secrets, 16 informers were enough to exhaust memory and crash the server. Streaming lists, beta in 1.32, serve the initial state as watch events from the watch cache and held memory at about 2 GB against about 20 GB without them (Kubernetes blog).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Short polling | Trivial, cacheable, stateless | Wasted requests, delay of half the interval | Data changes slowly or staleness is fine |
| Long polling | Near-instant, works on any HTTP path, each request independent | A request per batch, parked connections, timeout tuning | Occasional events, server-to-server consumers, restrictive networks |
| Server-Sent Events | One stream, many events, auto-reconnect with Last-Event-ID | One direction only, some proxies buffer responses | Live feeds and notifications to browsers |
| WebSockets | Two-way, low overhead per message | Stateful connections, harder to balance and scale | Chat, games, collaborative editing |
| Webhooks | No connection held at all | Receiver must be reachable and verify signatures | Server-to-server notifications across companies |
Pitfalls in production
- No cursor. "Give me what is new" loses every event published between one response and the next request. Always poll from an explicit sequence number or version.
- Wait time longer than an intermediary's idle timeout. The load balancer cuts the request at 60 seconds and the client sees a
504or a reset, and retries in a loop. Keep the wait under the smallest idle timeout in the path, typically 25 to 30 seconds. - Client read timeout shorter than the server wait. The client gives up on a healthy poll at 10 seconds and retries forever. Set the client timeout to the wait time plus a margin.
- Reconnect storms. After a restart or a broadcast, every client re-polls at the same instant. Add jitter to waits and to reconnects, and exponential backoff on errors.
- Thread-per-request servers. Each parked request holds a thread and its stack, and the pool is exhausted at a few hundred clients. Use an event loop or async handlers for long-poll endpoints, or isolate them on their own tier.
- Waking the wrong node. In a fleet, a publish only wakes waiters on the node that received it. Route publishes through pub/sub so every node holding a relevant waiter hears about it.
Leadership lens
- Ask where long polling sits in the transport roadmap: it is often the right first step, and a cursor-based API makes a later move to SSE or WebSockets cheap.
- Long-poll endpoints break the usual "requests are short" assumptions in dashboards, autoscaling and load tests; make sure p99 latency alerts exclude intended waits.
- Agree on timeout numbers with the team that owns the load balancers and CDN, and write them down; a quiet change to an idle timeout can break every client.
- Plan deploys for connection-holding services: drain slowly and expect a reconnect wave, or every release looks like an incident.
Check yourself
Why does a long-poll client send a cursor instead of just asking for new events?
Show answer
Events can be published in the gap between one response and the next request, or a response can be lost. The cursor lets the server return everything after the last event the client actually saw.
A load balancer has a 60-second idle timeout and the server waits 90 seconds. What do clients observe?
Show answer
Quiet channels see their requests cut at 60 seconds with a gateway error or reset, then retry, so every idle client generates errors and wasted reconnects instead of clean empty responses.
Why is long polling a good fit for SQS consumers but a poor fit for a multiplayer game?
Show answer
Queue consumers receive occasional batches and need no persistent connection, so holding a request until work appears is ideal. A game sends many small messages per second in both directions, and paying a full HTTP exchange for each is too slow and expensive.
What should the server do when a client's cursor points before the oldest retained event?
Show answer
Return an explicit error telling the client to reload full state, never a partial list that silently skips events.
Sources and further study
Primary
- RFC 6202, section 5: HTTP best practices for long polling: connection limits, timeouts and intermediary behaviour, with the 30-second guidance.
- KEP-3157: streaming lists for informers: the design record for replacing LIST plus watch with one stream.
Systems docs
- AWS: ALB connection idle timeout: the 60-second default and the 1 to 4,000 second range that bound every wait.
- etcd: watch API and compaction: revision cursors and what a watcher sees after history is compacted.
- Socket.IO: How it works: long polling first, then the upgrade to WebSocket.
Books
- High Performance Browser Networking, Ilya Grigorik, chapter 15 "XMLHttpRequest": polling and long polling with XHR and their costs (read free).
- Designing Data-Intensive Applications (1st ed.), Martin Kleppmann, chapter 11 "Stream Processing": logs, consumer offsets and why a cursor makes redelivery safe.
Other curricula
- Azure Architecture Center: Asynchronous Request-Reply: polls a status endpoint after
202 Acceptedand notes long polling as the lower-latency variant. - roadmap.sh: Backend: groups long and short polling with SSE and WebSockets under "Real-Time Data".
Watch
- Long Polling and how it differs from Push, Poll and SSE (Hussein Nasser): where long polling sits among the backend communication patterns.
- The genius of long polling (Hussein Nasser): why the pattern works so well for queues and job consumers.
- HTTP Request vs HTTP Long-Polling vs WebSocket vs Server-Sent Events (Amit Shekhar): a side-by-side comparison of the four transports.
Free resources
- RFC 6202: Known Issues and Best Practices for Long Polling and Streaming: the original catalogue of timeouts, connection limits and proxy behaviour.
- Consul blocking queries: a production long-poll API with index cursors and jitter.
- Amazon SQS short and long polling: how long polling cuts empty responses and cost for queue consumers.
- Kubernetes API concepts: efficient detection of changes: watches,
resourceVersioncursors and what happens when history is gone. - MDN: Using server-sent events: the streaming alternative and its reconnect model.
- Telegram Bot API: getUpdates: a simple public long-poll API with
offsetandtimeout.