Long polling problems in JavaScript and how to handle them

Última actualización: 01/23/2026
  • Long polling simulates server push over HTTP but suffers from header overhead, timeout quirks, and resource constraints.
  • Proxies, browser connection limits, and caching can silently break or degrade long polling behavior at scale.
  • Protocols like Bayeux and BOSH build on long polling to offer bidirectional messaging, often alongside streaming.
  • Optimized timeouts, batching, compression, and careful retry logic are essential to keep JavaScript long polling robust.

Long polling problem in JavaScript

Long polling in JavaScript looks deceptively simple: keep an HTTP request open until the server has something to say, return the data, and immediately open a new request. From the outside it feels almost like real-time communication, so it’s no surprise that chat apps, dashboards, games, and notification systems have relied on it for years. But under the hood there are subtle performance, scalability, and reliability pitfalls that bite as soon as your app grows beyond a handful of users.

Understanding the real problems of long polling in JavaScript means going beyond the basic “client sends request, server waits, server responds” description and looking at HTTP semantics, browser limits, proxies, timeouts, and the alternatives like WebSockets, HTTP streaming, and Server-Sent Events. In this guide we’ll dig deep into how long polling works, which issues have been identified by the IETF and major vendors, and how to decide when to use it, when to optimize it, and when to replace it entirely.

What long polling in JavaScript actually is

Explanation of JavaScript long polling

In plain terms, long polling is a pattern built on top of normal HTTP where the browser sends a request and the server deliberately delays the response until new data is available or a timeout expires. As soon as the browser receives a response, it processes the data and immediately fires off another request, keeping the connection “almost always” pending.

This flow is very different from classic short polling, where the client hammers the server on a fixed interval asking “anything new?” regardless of whether there are updates. With long polling, the server holds the request open during periods of inactivity, so the client doesn’t need to repeatedly send empty polls when nothing has changed.

A typical JavaScript long polling loop looks like a recursive subscribe function that calls fetch (or XMLHttpRequest), waits for the response, handles the payload, and then calls itself again. If the network drops or an error occurs, the code retries immediately or after a small delay, trying to keep that pending connection alive as much as possible.

This approach works particularly well when messages are relatively rare, because idle time on the connection doesn’t produce network traffic or CPU load beyond the cost of keeping a socket open. The user experiences near-instant updates, while the server avoids being bombarded with constant polling checks.

However, as soon as events become frequent, each event tends to trigger a whole HTTP response cycle—status line, headers, authentication, body—so the cost per message can explode and the traffic pattern starts to look like a saw-tooth of request/response spikes. That’s where problems with scaling and overhead begin to appear in real-world apps.

Short polling vs long polling vs streaming

Comparison between long polling and alternatives

To really understand the problem space, it helps to contrast long polling with both short polling and HTTP streaming, since all three are ways to fake “push” over a fundamentally request/response protocol like HTTP.

Short polling is the naive variant: the client periodically sends HTTP requests (for example every 5-10 seconds) and the server immediately replies with any messages that arrived since the last poll. If nothing is available, the server still responds, often with an empty payload, and the client simply waits until the next interval.

This model has two obvious downsides: message latency can be as high as the polling interval, and the server is forced to handle repeated requests even when there is zero new data. For small or low-traffic systems this might be acceptable, but at scale it wastes CPU cycles, network bandwidth, and can introduce noticeable delays for users.

Long polling improves on this by letting the server “hold open” each request until something interesting happens—an event to deliver or a timeout threshold. The latency for new messages is now close to a single network round trip, and empty responses happen much less frequently, reducing wasted traffic.

HTTP streaming goes a step further by never closing the response at all: the server sends one HTTP response and then streams multiple chunks of data over the same connection, separated by some framing at the application level. The client reads the stream incrementally instead of waiting for the entire response to finish.

In HTTP/1.1 this is usually implemented via chunked transfer encoding, where the server sets Transfer-Encoding: chunked and then sends each piece of data as a separate chunk with its own length header. In HTTP/1.0 style setups, streaming can also be achieved by omitting Content-Length and using connection close as the end-of-stream marker.

The crucial difference is that long polling closes the response after each message, forcing a new request for the next event, while streaming keeps the same response alive and pushes messages as they occur. That has major implications for latency, memory usage, proxies, and how you frame messages at the application layer.

How HTTP long polling works under the hood

From the perspective of the HTTP protocol, long polling doesn’t introduce any new methods or status codes; it just stretches the usual semantics of a request that waits for a response. The IETF’s analysis of “bidirectional HTTP” makes it clear that long polling is still valid HTTP 1.0/1.1, but pushes the model close to its limits.

The basic lifecycle of a long polling interaction looks like this: the client sends an initial request and pauses, the server defers the response until an event, status change, or timeout happens, then the server returns a full HTTP response (often 200 OK) with the event data, and finally the client quickly issues a new request.

This pattern can run over either persistent or non-persistent HTTP connections; with persistent connections you avoid the overhead of repeated TCP handshakes by reusing the same socket for multiple long polls. In practice, keeping the same TCP connection alive is almost always preferable for performance.

Servers that implement long polling usually must manage two resource buckets per client: the TCP connection itself and the outstanding HTTP request waiting in some queue or event loop. Operating systems can typically handle large numbers of open sockets efficiently, but some HTTP servers or gateways allocate significant per-request memory, which can become the real bottleneck.

One interesting property of long polling is that under heavy load it tends to degrade gracefully by increasing latency rather than failing hard. If the server is slow, messages destined for a client will queue up until a response can be sent; multiple queued events may even be batched into a single long poll response, which incidentally reduces per-message overhead.

Issues and limitations of long polling

Despite being widely used and standardized in practice, long polling introduces several well-known technical issues that are easy to hit from JavaScript if you’re not careful. These problems show up in bandwidth usage, latency, connection management, and the behavior of intermediaries like proxies and caches.

Header overhead is the first cost you run into: every long poll request and response is a complete HTTP message, typically with cookies, auth headers, and other metadata that might dwarf the actual payload. For small, infrequent messages this overhead may be acceptable, but in volume-based billing scenarios or bandwidth-constrained networks, the difference between header size and payload size can become expensive.

Maximal latency is another subtle issue: while the average latency of long polling for new events is roughly one network transit, the worst-case delay can be more than three transits. If a message arrives right after the server sent a response, the server must wait for the next request from the client before it can deliver that message, and TCP packet loss or retransmission can extend this time further.

Connection establishment is often raised as a concern, especially when people compare long polling to WebSockets. If every long poll response caused the HTTP connection (and underlying TCP connection) to be closed, the cost of repeatedly opening sockets would be huge. Fortunately, long polling can and should be layered on top of persistent connections so that the brief gap between responses is not interpreted as idleness; this lets the transport remain open and reusable.

Server and proxy resource allocation is a major practical limitation: each outstanding request consumes memory and possibly a thread or worker in synchronous architectures. Many older or blocking servers simply don’t scale to tens of thousands of concurrent long polls because their concurrency model expects each request to complete quickly; for these systems, asynchronous I/O or an event-driven design is almost mandatory.

Caching behavior can also break long polling if not explicitly suppressed. Intermediary caches may decide to reuse or store responses unless you clearly mark long poll requests and responses with Cache-Control: no-cache (and related headers), ensuring every request actually reaches the origin server and isn’t served stale data.

Timeouts, proxies, and intermediary behavior

One of the most annoying real-world problems with long polling in JavaScript is that you don’t fully control the network between browser and server. Proxies, gateways, load balancers, and even browser defaults all impose timeouts or buffering strategies that can break the illusion of a live connection.

Long poll requests are supposed to stay open until an event or a timeout that you control, but in reality many intermediaries will cut off connections after a shorter, fixed period. While browsers might allow up to around 300 seconds by default, some proxies enforce much smaller timeouts, which means your long poll will terminate with HTTP 504 Gateway Timeout or just a reset, forcing the client to reconnect more often than you intended.

Experiments and operational experience suggest that timeouts around 30 seconds are a safer compromise in many environments, with 120 seconds often working but being more fragile. Network equipment vendors that want to be long-poll-friendly are encouraged to use timeouts significantly longer than typical medium network transit times so that legitimate long polls aren’t killed prematurely.

Reverse proxies and connection pooling introduce another class of issues. Some proxy setups share a small pool of upstream connections between many clients; if long polls occupy those shared connections for extended periods, other requests may be starved or queued behind them, undermining performance for the entire site.

With HTTP streaming, intermediary buffering is an even bigger risk because proxies are allowed to buffer partial responses and only forward data once they’ve accumulated a complete response. In that case, a streaming server that sends small JavaScript chunks expecting immediate execution in the browser might find that an intermediary holds everything until the connection ends, completely destroying real-time behavior.

Browser limits and connection counts

From JavaScript, you never directly manipulate TCP connections; you only see high-level constructs like fetch or XMLHttpRequest, but browsers do enforce limits on how many parallel connections can be opened to the same host. Historically this limit was two connections per origin, which made Comet-style techniques particularly tricky.

Modern browsers have relaxed these constraints to around six or eight connections per host, but there’s still no standard way from JavaScript to ask “how many connections are left?” or to coordinate usage across tabs and iframes. Each tab might independently spin up its own long poll, quickly exhausting the available slots and blocking other essential requests like CSS, images, or API calls.

A best practice on the server side is to use cookies or some correlation mechanism to detect multiple long polls coming from the same browser instance and avoid deferring all of them. By responding quickly to extra long polls and only truly hanging one or a small number, you can reduce the risk of connection starvation or pipeline interference.

Some higher-level protocols built on long polling, such as Bayeux and BOSH, explicitly design around browser connection limits. They often use at most two outstanding HTTP requests at any one time and avoid relying on HTTP pipelining, which is poorly supported and not controllable from JavaScript.

HTTP pipelining, framing, and message ordering

While HTTP pipelining (sending multiple requests back-to-back on the same connection before getting responses) could in theory reduce long polling latency, in practice it’s fragile and inconsistently implemented. RFC 2616 is cautious about pipelining, especially for POST requests, and intermediaries or clients may disable it altogether.

Protocols that try to exploit pipelining for long polling must first detect that pipelining is reliably supported end-to-end; if not, they fall back to conservative, non-pipelined behavior. From a browser’s JavaScript environment, you don’t have the hooks to manage this, so most JavaScript-based long polling stacks simply assume pipelining is unavailable.

Framing—the way you split a continuous stream of bytes into distinct application messages—is another subtle difference between long polling and HTTP streaming. With long polling, each HTTP response naturally carries exactly one or a batch of messages, so your framing is implicit: one response equals one chunk of meaningful data.

In streaming, you can’t rely on HTTP chunk boundaries as your application framing, because proxies might re-chunk the data stream, merge chunks, or split them differently. That means the app must embed its own delimiters or length prefixes in the payload and parse accordingly on the client.

Message ordering and reliability are not guaranteed by long polling itself; they depend on your application protocol and storage layer. If a client disconnects and reconnects mid-stream, you need explicit mechanisms (like sequence numbers or last-event IDs) to ensure no messages are lost or delivered out of order.

Security considerations for long polling in JavaScript

Long polling as a technique doesn’t change HTTP’s core security model, but the way it’s often implemented—especially in browsers using JavaScript—can open the door to additional risks.

Many cross-domain long polling solutions rely on executing JavaScript returned from the server, sometimes through JSONP-style callbacks or other dynamic script injection. If your server is vulnerable to injection attacks, an attacker can potentially smuggle arbitrary code into those responses, which the browser then runs with the page’s privileges.

Using HTTPS everywhere is non-negotiable: encrypting transport protects against man-in-the-middle tampering and eavesdropping on long-lived connections. Combined with robust authentication and authorization (for example, token-based auth and role-based access control), you can keep long polling endpoints restricted to intended clients.

Since long polling keeps many connections open for long periods, it can make DoS attacks more attractive: an attacker might attempt to exhaust server resources by opening many bogus long polls. Rate limiting, connection quotas per IP or token, and sensible timeouts are essential defensive measures.

Browser-specific threats like CSRF are usually less central for pure XHR/fetch long polls that don’t rely on ambient cookies, but if cookies are involved, you should still treat these endpoints as you would any other sensitive API. SameSite cookies, CSRF tokens where appropriate, and strict CORS policies are all part of a hardened setup.

Long polling vs WebSockets and Server-Sent Events

From a JavaScript developer’s point of view, long polling, WebSockets, and Server-Sent Events are all ways to achieve “real-time feeling” features, but the trade-offs differ quite a bit.

WebSockets establish a true persistent, full-duplex channel between browser and server. After the initial HTTP upgrade handshake, data frames can flow both ways without the overhead of HTTP headers for each message, minimizing per-message latency and bandwidth usage.

This makes WebSockets ideal for high-frequency scenarios such as multiplayer gaming, collaborative editing, or telemetry streams where messages are small but frequent. The downside is that some corporate firewalls, old proxies, or legacy clients still don’t play nicely with WebSocket upgrades, and servers must use a different programming model and infrastructure tuned for large numbers of long-lived sockets.

SSE is simpler than WebSockets when you only need server-to-client push, but it cannot send messages from the browser back over the same channel; you still rely on normal HTTP requests for client-to-server communication. It also requires that intermediaries tolerate streaming and not excessively buffer partial responses.

Compared to these, long polling is often the “lowest common denominator” that works in more environments, including older browsers and conservative network setups. That’s why many real-time platforms historically used long polling as a fallback when WebSockets were blocked, gradually upgrading connections as capabilities allowed.

Real-world protocols on top of long polling: Bayeux, BOSH, SSE-like APIs

Several higher-level protocols have been built on top of long polling and streaming to hide the complexity from application developers and provide a consistent API. Understanding how they use long polling helps clarify both the strengths and weaknesses of the technique.

The Bayeux protocol, popularized by the CometD project, supports both HTTP long polling and streaming as transport options. A Bayeux client typically uses two HTTP connections to a server so that messages can flow in both directions asynchronously without being blocked behind long poll requests.

During an initial handshake, client and server negotiate which bidirectional techniques are supported—long polling, streaming, etc.—and the client picks one from the overlap. For JavaScript clients, Bayeux typically avoids depending on HTTP pipelining and instead limits itself to two outstanding requests to avoid browser connection limit issues.

BOSH (Bidirectional-streams Over Synchronous HTTP) comes from the XMPP world and was designed to emulate a TCP-like session over HTTP using long polling. A BOSH connection manager holds a client’s request open and responds only when it has data from the application server; as soon as the client gets that response, it sends a new request, keeping at least one pending long poll almost all the time.

BOSH can use one or two parallel HTTP request-response pairs, negotiated at the start of the session, and carefully manages them to respect browser connection limits while still allowing asynchronous bidirectional messaging. It forbids chunked transfer encoding to avoid buffering issues in intermediaries and permits compression via Content-Encoding for efficiency.

Server-Sent Events, though typically implemented via streaming rather than long polling, are closely related in spirit. The W3C specification describes using text/event-stream responses and suggests disabling HTTP chunking unless the event rate is high enough, again to avoid certain buffering and intermediary issues seen with streaming over HTTP/1.1.

Optimizing and hardening long polling in JavaScript apps

If you decide long polling is the right choice or a necessary fallback for your JavaScript app, there are several strategies to make it more efficient, scalable, and robust.

First, tune your timeouts thoughtfully. Too short and you’ll waste resources handling frequent reconnects and risk thundering herds of clients all reconnecting at once; too long and you increase the chance of hitting proxy or load balancer limits, causing mysterious disconnects. In practice, values in the 20-30 second range for WaitTimeSeconds (in APIs like Amazon SQS) and similar app-level timeouts often strike a good balance.

Next, consider batching events on the server side when multiple messages are queued for a client. Delivering several updates in a single long poll response dramatically reduces per-message overhead and can help the system degrade more gracefully under load by trading latency for throughput.

Compression is another easy win: enabling gzip or similar content encoding for JSON payloads over long polling can shrink bandwidth usage, especially when messages share repetitive structure. The trade‑off is extra CPU cost for compression, but in many real deployments that’s outweighed by reduced network transfer time.

From the JavaScript side, robust error handling and retry logic are non-negotiable. Your subscribe loop should detect network errors, timeouts, or malformed responses and retry with backoff rather than hammering the server. Exponential backoff with jitter is a common pattern to prevent synchronized retry storms during outages.

Finally, be disciplined about connection cleanup when components unmount or tabs close. Zombie polling loops that continue running in the background can chew through both client resources and server capacity, so always make sure you have a mechanism to cancel outstanding fetches or abort controllers when a view is destroyed or a user navigates away.

Long polling in JavaScript remains a powerful tool for building near real-time features in environments where WebSockets or SSE are unavailable, but it comes with a bundle of hidden costs around headers, timeouts, proxies, and resource usage that you need to understand and manage if you want your app to scale smoothly.

flask
Related article:
Flask and Server-Sent Events: Real-Time Patterns, Tools, Trends
Related posts: