R6.05.3 · Worker concurrency — dive
Capacity planning.
Plan worker capacity from the structural knobs, not a benchmark: pool width is the
concurrent in-flight count, :beat_ms and :lease_ms set the claim cadence and the reap
window, :pump_batch is the release-side knob, and the rate limiter is the ceiling where the
pool is the floor. Reason in round-trips — how many are outstanding, and how many jobs each carries
— not in measured milliseconds.
Grounding: echo/apps/echo_mq/lib/echo_mq/consumer.ex · the start_link tuning options; pump.ex · the release cadence (:pump_batch is the promote LIMIT per beat). EchoMQ.Pump is the release cadence, never the worker claim loop.
The knobs are on the consumer
consumer.ex · start_link optionsCapacity is configured, not discovered. EchoMQ.Consumer exposes the tuning surface as
start_link options, and each one names a structural property of the loop:
start_link(opts): :queue — the queue this loop drains :handler — fn %{id:, payload:, attempts:, group:} -> :ok | {:error, reason} :conn | :connector — the dedicated connector (or options to start one) :lease_ms — the lease window a claimed job holds before it is reapable (30_000) :beat_ms — the cadence: how often the loop reaps, promotes, drains, parks (1_000) :retry_delay_ms — the back-off applied to a failed job before it is eligible (1_000) :max_attempts — how many tries before a job is dead-lettered (3) :pump_batch — the promote LIMIT per beat (the release-side knob, 100) :metronome — opt-in: run the POOL path, fed by the queue's one blocker
These are levers on the shape of the work, not on the speed of the machine. Pool width — set when the pool is started, not on the consumer — is the count of connectors, and so the count of round-trips that can be outstanding at once. Together they describe the capacity of a deployment without measuring it.
The knobs are EchoMQ.Consumer options plus the pool width: the lease window, the
beat, the retry back-off, the promote batch. Each names a structural property of the loop — capacity is
configured, not discovered by a benchmark.
Reason in round-trips
the planning unitThe honest unit for planning is the round-trip, because it is the cost the design controls. A claim is one
round-trip and carries one job; a batch claim is one round-trip and carries up to k jobs. So the
throughput a deployment can offer is, in round-trips:
- Pool width sets how many claim round-trips are outstanding at once. A pool of four carries four; doubling the pool doubles the outstanding claims, until the server or the network is the limit.
- Batch size sets how many jobs each claim round-trip carries. A batch of eight divides
the fetch cost over eight jobs;
bclaim/3is the surface. - Claim cadence (
:beat_ms) sets how often a consumer reaches for work when the queue is quiet. A parked consumer spends no round-trips waiting, so the beat governs the housekeeping rhythm, not the busy-path throughput.
The product — outstanding claims times jobs per claim — is the structural throughput. Measured milliseconds are a property of a particular machine, network, and load; they are what a benchmark reports, not what a capacity plan is built from. The plan is in round-trips, and the round-trips come from the knobs.
Plan in round-trips, the cost the design controls: outstanding claims (pool width) times jobs per claim (batch size) is the structural throughput. A measured millisecond is a benchmark of one moment; the round-trip plan holds across moments.
The lease window and the release knob
consumer.ex · pump.exTwo knobs are about correctness under failure, and they bound the plan. :lease_ms is the
window a claimed job holds before it is considered stalled and reaped. Set it shorter than the time a healthy
handler takes and healthy jobs are reaped mid-flight and run twice; set it far longer than the handler and a
genuinely stalled worker holds its jobs for too long before another worker recovers them. The lease is sized
to the handler’s real working time with headroom, and a larger batch claim multiplies the leases a
single stall can strand.
:pump_batch is the release side, not the claim side. The release cadence —
EchoMQ.Pump — promotes due scheduled work and fires repeatables on its own beat, and
:pump_batch is the promote LIMIT per beat: how many due entries are released into the pending set
at once. It governs how fast delayed work becomes claimable, which is upstream of the workers.
EchoMQ.Pump is the release cadence, not the dequeue — it is cited here
only as the release-batch knob, and never as the worker claim loop.
:lease_ms is sized to the handler’s real working time with headroom —
too short reaps healthy jobs, too long strands stalled ones. :pump_batch is the release-side
promote LIMIT, upstream of the workers; EchoMQ.Pump releases work, it does not claim it.
Ceiling and floor
the pool vs the limiterThe pool and the rate limiter pull in opposite directions, and a plan uses both. Pool width and batch size are the floor: they set how much throughput the workers can offer. The rate limiter is the ceiling: a per-window counter that caps how much work is admitted per unit time, regardless of how wide the pool is. A deployment sizes the pool to keep up with demand and sets the limiter to protect a downstream that cannot absorb the full rate.
So capacity planning is two questions, not one. Can the workers keep up — a floor set by pool width, batch size, and the lease window. And should they be allowed to — a ceiling set by the rate limiter. The floor is concurrency made parallel; the ceiling is a budget per window. Neither is the count of BEAM processes.
Plan capacity from the system’s structure, not a one-off measurement: the levers that set how much work can flow, and the cap that limits how much is allowed.
Pool width, batch size (bclaim/3), the claim cadence (:beat_ms), and the lease
window (:lease_ms) are the floor reasoned in round-trips; the rate limiter (a per-window
counter) is the ceiling that caps the result from above.
Capacity is two questions: can the workers keep up — the floor of pool width, batch size, and the lease window — and should they be allowed to — the ceiling of the rate limiter. The floor is concurrency made parallel; the ceiling is a budget per window.
The pattern, applied
echo/apps/codemojexIn codemojex the scoring consumers drain the cm queue. The plan is the pool width for the
scoring fleet, the lease window sized to the scoring handler, and :pump_batch for how fast timed
game events release work — all reasoned in round-trips, so the plan holds whatever the day’s
network latency happens to be. A benchmark would report a number for one moment; the structural plan holds
across moments.
Sizing a pool against a real production load, recovering stalled leases at scale, and the metrics that confirm the plan are the queue’s scaling layer, taught in the EchoMQ course.
Sizing against real load
The Queue pillar sizes a pool against a production load: the metrics per queue, the lease recovery for a stalled worker, and confirming the plan in deployment.
/bcs/busThe bus the workers drain
Part B3 builds the Valkey-native bus the consumers claim off: a queue is a set of lanes, served on a server-clock lease the plan is sized around.
R6.01The ceiling above the floor
The rate limiter is the ceiling the pool’s floor runs beneath: a per-window counter that caps admitted work, however wide the pool.
Notes on ValkeyBLPOP lets the cadence cost nothing on a
quiet queue: a parked consumer holds the connection on the wake key and spends no round-trips until work arrives
or the beat elapses, so :beat_ms governs housekeeping, not busy-path throughput —
valkey.io/commands/blpop.
References
sources · relatedSources
- Valkey — BLPOP — the blocking park that makes the claim cadence free on a quiet queue.
- Valkey — ZPOPMIN — the per-claim fetch whose count, outstanding, is the unit of a capacity plan.
- Valkey — Pipelining — the round-trip model that makes outstanding claims the honest planning unit rather than measured time.
Related in this course
- R6.05 · Worker concurrencythe module hub
- R6.05.1 · Parallel vs concurrentpool width as the real parallelism the plan sizes
- R6.05.2 · The per-claim fetch bottleneckbatch size as the knob that divides the fetch cost
- R6.01 · Rate limitingthe ceiling the pool’s floor runs beneath
- /echomq/queuesizing a pool against a real production load
- /bcs/busPart B3, the Valkey-native bus the consumers drain