Long Polling: When the Server Delays the Response
Polling has a simple problem:
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:
GET /events?after=1842
If there is no new data, the backend does not answer right away.
client -> request ────────────────┐
│
wait │
│
event! │
client <- response ───────────────┘
A sample response:
{
"id": 1843,
"type": "order.completed",
"orderId": 42
}
Once it arrives, the client immediately starts the next request:
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:
request
|
v
event appears -> respond
or
timeout -> empty response
In both cases the client starts the next request.
For example:
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:
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:
GET /events?after=1842
After receiving 1843 the next request will be:
GET /events?after=1843
This introduces notions that come back later:
event identity
cursor
resume
duplicate delivery
Transport and delivery semantics are two different things.
Fewer requests, but more open requests
Polling:
few open requests
many total requests
Long polling:
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:
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:
event A
event B
event C
long polling usually runs a cycle:
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.