Redis Patterns/R4/The delayed queue/The next wake
R4.01.3 · The delayed queue — dive 3
The next wake.
Do not busy-poll an empty set. Read the head, learn when the next job is due, and wait that long. EchoMQ holds one block per queue, woken by the admit that creates work.
Grounding: echo/apps/echo_mq/lib/echo_mq/metronome.ex · the single BLPOP emq:{q}:wake block · EchoMQ.Jobs.reap/2 + promote/3 per beat — the real wake, on the bus’s own pump.
LPUSH releases the metronome’s single block at once; otherwise the
BLPOP times out on the bounded beat and runs the promote sweep anyway.Static verdicts — admit → LPUSH emq:{q}:wake 1 releases the block
now · beat → BLPOP emq:{q}:wake <beat> times out, then reap + promote run on the
fallback.
The cost of busy-polling
delayed-queue.md.txt · Avoiding Busy WaitingPolling an empty set on a fixed tick wastes work. Every tick the worker runs the due-range, gets nothing back, and sleeps a fixed interval — then repeats, whether or not anything has changed. The shorter the tick the more round trips wasted on an empty set; the longer the tick the later a due job is noticed. A fixed poll cadence cannot be both responsive and cheap.
The waste is not a job that runs slowly; it is a worker spending round trips to learn nothing. On an idle set, every poll returns the same empty due head, and the interval between polls is latency added to whatever job lands next.
A fixed poll cadence trades responsiveness against cost: a short tick wastes round trips on an empty set, a long tick adds latency to the next due job.
Read the head, compute the next wake
delayed-queue.md.txt · the headThe set already holds the answer. Its head is the earliest-due job, and its score is when that job is due:
ZRANGE delayed_queue 0 0 WITHSCORES # the earliest task and its scheduled time
When the head’s time is at or below now, a job is due and the worker sweeps it. When the head is still
in the future, the worker waits until that time, or a bounded maximum interval, whichever is sooner — and
proceeds with a job due, having spent no round trips in between. The wait is computed from the head’s
score: next_wake = head_score - now. Toggle the two timings below to compare the wasted polls of a
fixed tick against the single block an admit releases.
Static verdicts — poll → one round trip every tick across the window, each
finding the empty due head · metronome → one block, released by the next admit’s
LPUSH or by the beat — no round trip in between.
The set already names its next wake: the head’s score is when the earliest job is due. Holding one block instead of polling on a tick makes an idle set nearly free.
EchoMQ’s metronome: one block, woken by the admit
echo_mq/metronome.ex · the beatEchoMQ does not run a poll loop per worker. It holds one block per queue in EchoMQ.Metronome
— a single process that owns the only BLPOP emq:{<queue>}:wake <beat> and a
registry of idle consumers. The beat is a bounded fallback; the real wake is an admit. When a job is admitted,
the producer LPUSHes the wake token, and the single block returns at once. Each beat —
whether woken by an admit or by the timeout — the metronome runs the reap and promote pumps before poking
the idle consumers:
# EchoMQ.Metronome (metronome.ex) — the beat (real) {:ok, _} = Jobs.reap(conn, queue) # return expired leases to pending {:ok, _} = Jobs.promote(conn, queue, pump_batch) # promote the due schedule head # then hold the single block until the next admit or the beat timeout: Connector.command(conn, ["BLPOP", Keyspace.queue_key(queue, "wake"), secs], beat_ms + 2_000)
So promotion runs on demand. An admit that schedules a job LPUSHes the wake token; the
metronome’s single block returns; the beat runs promote/3, which sweeps any due head onto the
pending set; and the metronome pokes the idle consumers, each running its byte-frozen claim once. The herd of
pollers is gone — one connection blocks, one beat sweeps, and the wake comes from the work. The
beat_ms (default 1_000) bounds how long an idle queue waits before a fallback sweep;
pump_batch (default 100) caps how many due jobs one beat promotes.
Notes on ValkeyBLPOP blocks the connection until a list
has an element or the timeout elapses, so one block per queue replaces a herd of pollers — the admit’s
LPUSH on the wake token releases it at once —
valkey.io/commands/blpop.
The wake, the promote pump, and the consumer pool
This dive cites the metronome’s single BLPOP block and its per-beat
reap/promote as proof the wake ships. The whole pump — the consumer registry,
the fair-poke dispatch, the lifecycle controls — is the dedicated
EchoMQ Queue course. This module teaches the wake; that course teaches the
metronome that holds it.
Do not poll an empty delayed set; read the head, compute when the next job is due, and wait until then rather than re-polling on a tick.
EchoMQ.Metronome holds one BLPOP emq:{<queue>}:wake <beat> per
queue, and each beat runs EchoMQ.Jobs.reap/2 then EchoMQ.Jobs.promote/3; an
admit’s LPUSH on the wake token releases the single block at once, so the promote sweep
runs on demand.
The head’s score is the next wake; holding one metronome block per queue beats polling on a
tick, and an admit’s LPUSH on the wake token releases it the instant there is something to
promote.
When a deferred job must survive a crash
delayed-queue.md.txt · the durable floorA scheduled job lives on the schedule set in Valkey, which is fast but volatile — if the
process holding it dies, an in-flight schedule entry is only as durable as the bus’s own persistence. For
work that must survive a crash, the deferred run is folded onto the durable substrate beneath the bus: an ETS
head over a Valkey L2, a durable local page tier, and a remote object store, with durability a dial
a system turns rather than a fixed cost on every enqueue.
The durable floor a scheduled job reaches
The mechanics of that floor — the single-writer page engine, the lazy reader, the create-only commit fence — are a course of their own. Open the /echo-persistence course where a scheduled or retried job reaches the durable floor, and /bcs · Persistence for the same substrate read as architecture. This module schedules and wakes the job; that course makes the schedule durable.
The schedule set is fast but volatile; for a deferred run that must outlast a crash, the durable
floor turns the durability dial — the subject of /echo-persistence.
References
sources · relatedSources
- Valkey — BLPOP — the blocking pop the metronome holds on the wake token, so one block per queue replaces a herd of pollers.
- Valkey — LPUSH — the ring an admit writes to the wake token to release the single block at once.
- Redis — ZRANGE — read the schedule head and its score: the earliest job and when it is due.
- Redis — Sorted sets — the data type behind the schedule set’s head the wake reads.
Related in this course
- R4.01 · The delayed queuethe module hub
- R4.01.1 · The score is the fire-timethe run-at score the head carries
- R4.01.2 · ZRANGEBYSCORE promotionthe sweep the metronome beat runs
- R3.05 · Blocking vs pollingthe blocking-pop the wake is built on
- /echo-persistencethe durable floor a deferred job reaches
- /echomq · Queuethe metronome and the promote pump in depth