jonnify·redis /redis-patterns/queues/blocking-vs-polling/the-busy-poll-cost

R3.05.1 · Blocking vs polling — dive 1

The busy-poll cost.

A worker that takes the next job by polling pays a wasted round-trip on every empty poll and about half an interval of pickup latency. The interval is the only dial, and it has no good setting.

Grounding: the polling loop the source's Blocking Variant section replaces · the contrast is the real Elixir EchoMQ.Consumer.park/1 (BLPOP emq:{queue}:wake, echo/apps/echo_mq) — "park, don't poll."

Trace one poll, empty or pick. Select an outcome to light it and read the round trip it makes.
worker · two checks emptypop → nil pickpop → job
readout · Select a poll outcome. An empty poll is a wasted round trip; a pick returns a job.

Static verdicts — an empty poll → one round trip, no work · a pick → one round trip, one job, but the job waited in the gap before this poll.

§1

The polling loop

reliable-queue.md.txt · the loop replaced

The naive wait is a spin. Each pass sends a pop command and, when the queue is empty, gets a nil back. That nil is a wasted round-trip — a full command-and-reply over the wire for no work. The worker then sleeps the interval and tries again. The work of checking whether a job exists is paid separately from the work of doing the job, and on an idle queue only the checking happens.

# the polling loop: spin, sleep, retry — every nil pass is a wasted round-trip
loop:
  job = pop work_queue -> processing
  if job == nil:
    sleep(interval)          # burn the interval, then poll again
    continue
  process(job)

The second cost is latency. A job rarely arrives exactly when the worker polls. It arrives in the gap between two polls and waits the rest of that gap before the next poll picks it up, so average pickup latency is about half the interval.

empty poll
A pop that returns nil. One round trip, no work — pure overhead on an idle queue.
pickup latency
The time a job waits between arriving and the next poll — up to one interval, about half on average.
interval
The sleep between polls. The only dial, and the one that sets the two costs against each other.

A polling loop pays a wasted round trip on every empty poll and adds about half an interval of pickup latency — two costs, paid at the same time.

§2

The interval has no good setting

no value is cheap on both axes

The interval is the single knob, and it sets the two costs against each other. A short interval cuts pickup latency but multiplies the empty round-trips on an idle queue — a 50 ms interval is twenty polls a second per worker, almost all of them empty. A long interval cuts the round-trips but lengthens pickup latency — a 1 s interval is one poll a second, but a job can wait up to a full second before pickup.

No value is cheap on both axes, because polling charges for checking and for doing as two separate things. Slide the interval below over a fixed window and an arrival rate; the readout computes the empty polls, the average pickup latency, and the round-trips per second. There is no minimum that satisfies both.

The poll-cost calculator. Set the interval and the arrival rate; the readout runs the cost over a one-second window.
200
2

Static verdicts — short interval → low latency, many empty polls · long interval → few polls, high latency. No setting is cheap on both.

The interval dials latency against round-trips and there is no minimum that satisfies both: the wasteful end and the laggy end are the only choices polling offers.

§3

The way off the trade — park

consumer.ex · park/1

The only way off the trade is to stop checking on a cadence at all — to park until there is something to do. EchoMQ.Consumer does that: its loop drains the ring, then parks on BLPOP emq:{queue}:wake <beat> instead of sleeping. There is no spin and no empty round trip, and a wake returns it the instant work is admitted — pickup latency near zero, not half an interval. The beat is the timeout, so the loop still runs its reap and promote pump on a cadence; the wake handles the fresh arrivals.

Notes on ValkeyA pop on an empty list returns nil immediately, so a polling loop must add its own sleep to avoid a tight spin — the blocking forms remove that sleep by parking the connection on the server — valkey.io/commands/blpop.

the pattern

A pop/sleep loop pays a wasted round trip on every empty poll and about half an interval of pickup latency; the interval dials one cost against the other, and no setting wins.

its EchoMQ application

EchoMQ.Consumer does not poll. Its loop drains the ring, then parks on BLPOP emq:{queue}:wake <beat> — no spin, no empty round trips, and a wake returns it the instant work is admitted.

Polling has no good interval; the next dive builds the blocking park that removes the spin entirely.

§4

Recap — the spin no interval fixes

next → R3.05.2

A polling loop pops, and on nil sleeps the interval and retries. Every empty pass is a wasted round trip, and a job waits up to one interval before pickup. The interval is the only dial, and it trades latency against round-trips with no value cheap on both. The next dive replaces the spin with a blocking pop that parks the connection until work arrives.

§5

References

sources · related

Sources

  • Redis — Documentation — lists, pops, and why a polling loop needs an explicit sleep.
  • Valkey — LPOP — the non-blocking pop a polling loop spins on; nil on an empty list.
  • Valkey — BLPOP — the blocking alternative that removes the interval and the empty round-trips.

Related in this course