Konrad Kowalski (rootsher)Principal Platform & Reliability Architect111111101011110111001010000001101011000101000110

Long Polling: When the Server Delays the Response

date
category
Backend
also in
Frontend
reading
2 min / 375 words

Polling has a simple problem:

text
client -> anything new?
server <- no

client -> anything new?
server <- no

The server answers immediately, even though the client does not need a "nothing changed" response.

Long polling changes when the answer arrives.

The request stays open

The client sends:

http
GET /events?after=1842

If there is no new data, the backend does not answer right away.

text
client -> request ────────────────┐
                                  │
                             wait │
                                  │
                          event!  │
client <- response ───────────────┘

A sample response:

json
{
  "id": 1843,
  "type": "order.completed",
  "orderId": 42
}

Once it arrives, the client immediately starts the next request:

js
async function listen(after) {
  while (true) {
    const response =
      await fetch(`/events?after=${after}`);

    const event = await response.json();

    handle(event);
    after = event.id;
  }
}

This is still HTTP request/response.

No persistent message channel appeared. The response can simply come much later.

The timeout is part of the protocol

No request should hang forever.

A reverse proxy, a load balancer or the backend itself may limit how long a request can last.

So the typical model looks like this:

text
request
  |
  v
event appears -> respond

or

timeout -> empty response

In both cases the client starts the next request.

For example:

text
t=0    request A
t=25   timeout
t=25   request B
t=41   event
t=41   response B
t=41   request C

Long polling is therefore a chain of long requests, not one infinite request.

We have to know what we already saw

Reconnection creates a real problem.

Suppose:

text
event 1843 generated
response sent
network breaks

Did the client receive the event?

The backend cannot conclude that reliably from the fact that it sent a response.

That is why the client often passes a cursor:

http
GET /events?after=1842

After receiving 1843 the next request will be:

http
GET /events?after=1843

This introduces notions that come back later:

text
event identity
cursor
resume
duplicate delivery

Transport and delivery semantics are two different things.

Fewer requests, but more open requests

Polling:

text
few open requests
many total requests

Long polling:

text
many open requests
fewer total requests

With many clients the backend and the infrastructure have to handle a large number of simultaneously pending I/O operations well.

In a thread-per-request model that can be expensive.

With event-driven, non-blocking I/O thousands of waiting sockets are far more natural.

So the communication mechanism directly affects the execution model of the backend.

Why did it work so well?

Long polling lets the server deliver a change almost immediately without requiring a separate realtime protocol.

With keep-alive set up correctly, consecutive requests can reuse the existing transport connection.

That is an important distinction:

text
HTTP request lifetime
!=
TCP connection lifetime

We end the request, not necessarily the physical connection.

But we still end the response

If the server has three events:

text
event A
event B
event C

long polling usually runs a cycle:

text
request
<- event A

request
<- event B

request
<- event C

Which leads to a simple question:

if an HTTP response body can be sent gradually, why do we have to end the response after the first event?

We do not.

A single response can be a stream of data.

That is HTTP streaming.