Konrad Kowalski (rootsher)Principal Platform & Reliability Architect110011000001101001000010000101101011000011111111

Polling: Building Realtime with Repeated Requests

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

The frontend fetches the status of an operation:

http
GET /orders/42

HTTP/1.1 200 OK
Content-Type: application/json

{"status":"processing"}

The request ends.

Five seconds later the backend changes the state:

text
processing -> completed

HTTP has no mechanism that would tell a finished request about it.

The simplest solution is to make another one.

Polling

js
const timer = setInterval(async () => {
  const response = await fetch("/orders/42");
  const order = await response.json();

  render(order);
}, 5000);

That produces a sequence:

text
t=0   GET -> processing
t=5   GET -> processing
t=10  GET -> processing
t=15  GET -> completed

This is still ordinary request/response.

"Realtime" comes solely from repeating the request often.

Latency is a function of the interval

If polling happens every T, a change can be noticed anywhere between almost immediately and almost T later.

With changes spread evenly over time the average extra delay is roughly:

text
T / 2

So polling every 10 seconds means about 5 seconds of delay on average, coming purely from the client's schedule.

Lowering T improves freshness but raises the cost.

The cost exists even without changes

Suppose:

text
100 000 clients
poll every 5 s

That gives on average:

text
100 000 / 5 = 20 000 requests/s

even if for most of the time the backend answers:

json
{"status":"processing"}

The problem is not the size of a single response.

The problem is the ratio:

text
number of requests
------------------
number of actual changes

If the state changes once every few minutes, most of the traffic exists only to confirm that nothing changed.

Clients can synchronize themselves

If thousands of UI instances start polling at a similar moment:

text
00s  ███████████████
01s
02s
03s
04s
05s  ███████████████

the backend gets periodic spikes.

That is why the interval is often worth spreading with jitter:

text
next_poll = base_interval + random_jitter

The same reason jitter shows up in retry/backoff.

Polling does not have to return full data

HTTP semantics can help here.

An example with ETag:

http
GET /orders/42
If-None-Match: "v17"

If the representation has not changed:

http
HTTP/1.1 304 Not Modified
ETag: "v17"

That reduces the body transferred, but not the number of requests.

It is also possible to ask for changes since a given point:

http
GET /events?after=1842

That avoids fetching the whole state, but the client still decides when to ask.

Polling has one important advantage

It is operationally simple.

Every request is independent:

text
client
  |
  v
load balancer
  |
  v
any backend instance

There is no multi-minute relationship between a client and one particular connection.

The ordinary things keep working:

  • timeouts,
  • HTTP caching,
  • autoscaling,
  • stateless backend instances,
  • classic reverse proxies.

That is why polling is sometimes the right choice.

If a dashboard can show data 30 seconds old, a WebSocket may be a more expensive answer to a problem we do not have.

Where is the real limitation?

The client asks:

text
anything new?
anything new?
anything new?
anything new?

For most of the time the server answers:

text
no
no
no
yes

But the server knows when the yes appears.

So one thing can change:

instead of answering "nothing new" immediately, do not answer until something happens.

That is long polling.