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."
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.
The polling loop
reliable-queue.md.txt · the loop replacedThe 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.
The interval has no good setting
no value is cheap on both axesThe 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.
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.
The way off the trade — park
consumer.ex · park/1The 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.
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.
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.
Recap — the spin no interval fixes
next → R3.05.2A 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.
References
sources · relatedSources
- 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
- R3.05 · Blocking vs pollingthe module hub
- R3.05.2 · Blocking popthe next dive: the primitive that removes the spin
- R3 · Reliable Queuesthe chapter
- /echomq/queuethe Queue pillar: the consumer loop in depth