R3.05 · Reliable Queues
Blocking vs polling.
Guarantee at-least-once message delivery using an atomic transfer to a processing list, enabling recovery if consumers crash before completing work.
The reliable queue answers where a job lives. This module answers a second question
it raises — how does a worker take the next job off an empty queue? A worker that spins a
pop/sleep loop on an idle queue burns a round-trip on every empty poll and picks a job up to one interval late.
The blocking variant replaces that spin: the consumer parks until work arrives. The grounding is the real
Elixir EchoMQ.Consumer (echo/apps/echo_mq) — BLPOP emq:{queue}:wake on a
dedicated connector lane, rung by the admit script's LPUSH, with the beat as the fallback cadence
and one EchoMQ.Metronome per pool. The worked consumer is codemojex.
The blocking variant
reliable-queue.md.txt · Blocking VariantThe source's reliable-queue loop, in polling form, pops the work list and falls back to a sleep: pop; if the
result is nil, sleep a fixed interval; retry. For efficient consumption without polling, the source
offers the blocking form — BLMOVE work_queue processing RIGHT LEFT 30 — which waits
up to thirty seconds for a message. If the queue is empty, the connection parks on the server rather than
returning immediately. There is no poll interval and no wasted round-trip: the call returns the instant an
element is available, or after the timeout elapses.
EchoMQ keeps the idea and changes the primitive. The consumer does not park on the work list. It parks on a
dedicated wake key, emq:{queue}:wake — a short capped list — with
BLPOP emq:{queue}:wake <beat>. When a producer admits a job, the admit script runs
LPUSH emq:{queue}:wake '1' (then LTRIM emq:{queue}:wake 0 63 to bound the list),
which returns the parked BLPOP; the consumer then drains the ring with
EchoMQ.Lanes.claim/3. Same trade as BLMOVE — park instead of poll, wake the
instant work arrives, burn no idle round-trips — applied through a separate signalling key, so a wake
fires no matter which lane the new work landed on.
Static verdict — poll → checks every interval, several empty, pickup up to one
interval late · park → one parked BLPOP, zero empty checks, returned at the arrival.
Polling trades latency for round-trips on one dial and pays both; parking returns the consumer the instant work arrives, paying neither. EchoMQ parks on a wake key, not the work list.
The busy-poll cost
the loop the Blocking Variant replacesThe naive wait is a spin: pop the work list; if the result is nil, sleep(interval); repeat.
Every pass that returns nil is a wasted round-trip — a command sent and a nil sent
back, for no work. On a queue that is empty most of the time, that is the steady state.
A job rarely arrives exactly when the worker polls. It arrives in the gap between two polls and waits the rest of the gap before the next poll picks it up, so average pickup latency is about half the interval. The interval is the only dial, and it has no good setting: a short interval cuts latency but multiplies the empty round-trips on an idle queue; a long interval cuts the round-trips but lengthens pickup latency. No value is cheap on both axes, because polling pays for checking whether work exists separately from doing the work. The first dive quantifies the trade with a poll-cost calculator.
Polling pays a wasted round-trip on every empty poll and adds about half an interval of pickup latency; the interval dials one cost against the other, and no setting wins.
Blocking pop
consumer.ex · the dedicated connector laneA blocking pop — BLMOVE, BRPOPLPUSH, or BLPOP with a timeout
— parks the connection on the server until an element is available or the timeout elapses. There is no
poll interval, so a job is picked up the moment it arrives, not up to one interval later. There are no empty
round-trips, because the call does not return until there is work or the timeout fires.
The cost is the connection. While a connection is parked on a blocking call, it is occupied — a command sent on the same connection waits behind the block. EchoMQ parks the block on a dedicated connector lane (the consumer's moduledoc: "a dedicated connector — blocking verbs get their own lane"), so the lane that drains and settles jobs is never tied up by a park. The second dive builds the primitive and the dedicated-connection discipline.
A blocking pop parks the connection until work arrives, removing the interval and the empty round-trips; the price is a tied-up connection, which a dedicated connector lane keeps off the drain lane.
The wake-up doorbell
consumer.ex · park/1 (BLPOP wake)EchoMQ's consumer loop makes the handshake precise. The loop is reap → promote → drain →
park, repeating: it reaps expired leases, promotes due schedules, drains the ring with rotating claims, then
parks on the wake key with BLPOP emq:{queue}:wake <beat> until readiness arrives as a wake
or the beat elapses.
# EchoMQ.Consumer.park/1 — park on the wake key for one beat, no spin (real, Elixir) defp park(s) do secs = :erlang.float_to_binary(s.beat_ms / 1000, decimals: 3) wake = Keyspace.queue_key(s.queue, "wake") # emq:{queue}:wake _ = Connector.command(s.conn, ["BLPOP", wake, secs], s.beat_ms + 2_000) :ok end
The producer side rings the doorbell from inside the admit Lua: pushing a job onto the ring runs
LPUSH emq:{queue}:wake '1', then LTRIM emq:{queue}:wake 0 63 to cap the list at 64
tokens (the same two calls appear in the promote and reclaim scripts in jobs.ex,
lanes.ex, and stalled.ex). The LPUSH returns the parked
BLPOP, and the consumer drains. The wake key is a signal, not a queue: BLPOP pops
one element, so one LPUSH returns one parked block. The beat (:beat_ms, default
1000) is the fallback: when no wake arrives, the BLPOP times out and the loop runs its
reap/promote pump anyway, so a due schedule or an expired lease is never stranded. The third dive walks the
full handshake and the pool's one-blocker EchoMQ.Metronome.
Notes on ValkeyBLPOP removes the first element of a list, or
parks the connection until one is pushed or the timeout elapses — the engine returns exactly one of
several connections parked on the same key, in the order they parked —
valkey.io/commands/blpop.
EchoMQ's consumer parks on BLPOP emq:{queue}:wake; the admit script's
LPUSH emq:{queue}:wake '1' rings the doorbell, the parked block returns, and the beat is the
fallback — one ring returns one consumer.
When to use
reliable-queue.md.txt · When to UsePark instead of poll whenever a worker waits on a queue that is empty much of the time and pickup latency
matters. A busy-poll loop is acceptable only when a blocking connection is not available, or when the
producer cannot ring a signal and the worker must re-check on its own cadence. Otherwise the blocking form
wins on both axes it trades against: lower latency and fewer round-trips at once. EchoMQ takes a third
position for a pool — one EchoMQ.Metronome holds the single block per queue and pokes idle
consumers, so the connection cost of blocking is paid once, not once per worker.
| Concern | Choice |
|---|---|
| Wait for the next job | park on a primitive with a timeout, not a pop/sleep loop |
| The primitive | BLMOVE/BRPOPLPUSH on a work list, or BLPOP on a wake key |
| The wake signal | the admit script LPUSHes the wake key (LPUSH emq:{queue}:wake '1', LTRIM … 0 63) |
| The connection | a dedicated connector lane, so the drain/settle lane stays free |
| Bound the wait | the beat (:beat_ms) is the timeout, so the loop re-runs its pump and is never parked forever |
| A pool | one EchoMQ.Metronome blocks per queue and pokes idle consumers — one blocker, no herd |
Park on a primitive with a timeout, ring the wake key when a job is admitted, and park the block on a dedicated lane — the choices this module's three dives make precise.
The three dives
cost → primitive → wakeEach dive takes one part of the move, in order: the cost the polling loop pays, the blocking primitive that removes it, and the EchoMQ wake handshake that returns the parked consumer.
The busy-poll cost
The naive pop/sleep loop: every empty poll is a wasted round-trip, a job waits up to one interval before pickup, and the interval is the only dial — no setting wins.
Blocking pop
BLMOVE/BRPOPLPUSH/BLPOP with a timeout park the connection
until work arrives — no interval, no wasted round-trips — on a dedicated connector lane.
The wake-up doorbell
The EchoMQ handshake: the consumer parks on BLPOP emq:{queue}:wake; the admit script
LPUSHes the key; the parked block returns; the beat is the fallback; the metronome blocks once
for a pool.
Park on a primitive with a timeout instead of polling, so the worker wakes the instant work arrives and burns no idle round-trips. Ring a signal when work is added, so one ring returns one parked worker.
EchoMQ.Consumer parks on BLPOP emq:{queue}:wake <beat> on a dedicated lane;
the admit Lua LPUSHes the key, the beat is the fallback, and one EchoMQ.Metronome
blocks once for a pool. codemojex's guess queue rides it.
The consumer loop, in depth
The wake key, the rotating ring claim, the dedicated connector lane, and the metronome are the Queue
pillar — the emq:{q}: protocol taught end to end.
The bus, in the manuscript
Part III builds EchoMQ on Valkey: jobs and lanes, the leased state machine, and the
emq:{q}: keyspace the wake key lives in.
References
sources · relatedSources
- Valkey — BLPOP — block until an element is pushed to a list; the park the consumer makes on the wake key.
- Valkey — LPUSH — push the wake token that returns a parked
BLPOP. - Redis — BLMOVE — the modern blocking reliable-queue pop the source uses.
- Redis — BRPOPLPUSH — the older blocking reliable-queue pop.
- Redis — Documentation — lists, blocking commands, and the reliable-queue access pattern in context.
Related in this course
- R3.05.1 · The busy-poll costthe cost the loop pays
- R3.05.2 · Blocking popthe primitive that removes it
- R3.05.3 · The wake-up doorbellthe EchoMQ wake handshake
- R3.01 · Processing listthe wait/active states the consumer drains
- R3 · Reliable Queuesthe chapter
- /echomq/queuethe Queue pillar: the consumer loop in depth