jonnifyredis
Redis Patterns / R3 · Reliable Queues / At-least-once / At-least-once semantics

R3.02 · dive 1

At-least-once semantics

One or more times, never zero. A reliable queue holds the job until its acknowledgement — the only choice that loses no job, and the choice that lets a job run twice.

Three guarantees bound every queue, set by one decision: does the job leave before the work or after the acknowledgement. At-most-once leaves before, so a crash loses it; at-least-once holds until the acknowledgement, so a crash redelivers it; exactly-once delivery is not on the spectrum at all. Toggle the delivery mode on the right over a fixed run where the worker crashes after the effect, before the acknowledgement, and read messages lost against messages run.

One run, the worker crashes after the effect · lost vs run, by mode

the job · one run, crash after the effect held until ack · crash redelivers messages lost 0 messages run (the effect) 2 at-least-once · never lost, possibly twice

The three guarantees

Three delivery guarantees bound every queue design, and the spectrum is set by one decision: when does the job leave the queue, before the work or after the acknowledgement.

At-most-once. The job is removed before the work runs — RPOP and process. A crash mid-job loses the job: zero deliveries. The guarantee is that a job is delivered no more than once; the price is that it can be delivered zero times. Acceptable only when a dropped job is harmless.

At-least-once. The job is held until the acknowledgement — moved into an in-flight list and only removed after the work confirms. A crash mid-job redelivers the job: one delivery or more. The guarantee is that a job is delivered at least once; the price is that it can be delivered twice. This is the reliable queue.

Exactly-once. One delivery and exactly one. This is what everyone wants, and it is not achievable as a delivery guarantee, because the acknowledgement itself can be lost — the third dive in this module is that argument. What is buildable is an exactly-once effect, by making the consumer idempotent on top of at-least-once delivery.

A reliable queue must keep the job until it is acknowledged, so it lands on at-least-once — the only point on the spectrum that never drops a job.

Why the move-under-lock yields at-least-once

R3.01 moved a job into an in-flight list under a lock instead of popping it out. LMOVE work active RIGHT LEFT (or the older RPOPLPUSH) makes the move atomic: the job is in exactly one of the two lists at every instant, never in neither. The lock has a TTL; a healthy worker renews it on a heartbeat. When a worker crashes, the heartbeat stops, the lock expires, and a recovery sweep finds the job still parked in-flight and returns it to the main queue.

The recovery path is what makes the queue reliable, and it is precisely why a job can run twice. A job in-flight is never lost — that is the win. But a job in-flight whose lock expired is redelivered — that is the cost. The two are the same mechanism viewed from two angles. You cannot keep the recovery and remove the redelivery, because removing the recovery is exactly what loses the job.

# at-most-once: the job leaves before the work — a crash loses it
job = RPOP work             # job is out of Redis
process(job)                # crash here → job gone, zero deliveries

# at-least-once: the job is held until the acknowledgement — a crash redelivers it
job = LMOVE work active RIGHT LEFT   # atomic: held in `active` under a lock
process(job)                         # crash here → job stays in `active`, recoverable
LREM active 1 job                    # acknowledge: remove only after the work confirms

Hold the job until the acknowledgement, and a crash leaves it recoverable rather than lost; recover it, and the same job runs again. The recovery is the reliability and the redelivery both.

The duplicate window

The duplicate is not a rare corner; it has a precise window. A worker takes a job into the in-flight list under a lock. It applies the effect — charges the card, writes the row. Then it crashes after the effect but before the acknowledgement (LREM). The effect is done; Redis does not record it. The lock expires. The recovery sweep finds a job in-flight with an expired lock and returns it to the main queue. A second worker runs it again. The card is charged twice.

This window is irreducible. No reordering closes it: if the acknowledgement comes first, the job leaves the in-flight list before the work, and a crash now loses it (back to at-most-once). The acknowledgement has to come after the work, which means there is always a moment where the work is done and the acknowledgement is not. A crash in that moment redelivers. The window is small, but it is never zero — and over enough jobs, a never-zero probability fires.

Slide the crash point across the run below. Only a crash inside the gap between the effect and the acknowledgement produces a duplicate; a crash before the effect loses nothing run, a crash after the acknowledgement is clean.

The crash point across one run · where the duplicate lives

3
one run · four steps · the crash lands on the highlighted step LMOVE effect LREM ack done ↑ the duplicate window a crash here re-runs the effect → the card is charged twice

The duplicate window is the gap between the effect and the acknowledgement, and no reordering closes it: the acknowledgement must follow the work, so a crash between them always redelivers.

In EchoMQ — the worker path that is at-least-once

EchoMQ's worker path is at-least-once in real code, in echo/apps/echomq. moveToActive-11.lua parks a job in emq:{queue}:active under a lock — its body runs rcall("RPOPLPUSH", waitKey, activeKey), where waitKey is emq:{queue}:wait and activeKey is emq:{queue}:active (EchoMQ.Keys.wait/1 and EchoMQ.Keys.active/1). The job is held in active until the worker acknowledges it; a crash leaves it there, recoverable.

-- the in-flight list and the recovery target — real EchoMQ keys
def wait(ctx),   do: "#{base(ctx)}:wait"     # emq:my_queue:wait — the main queue
def active(ctx), do: "#{base(ctx)}:active"   # held under a lock until acknowledged

moveStalledJobsToWait-8.lua is the recovery sweep: for each job in active whose lock has expired it returns the job to wait for another worker — and that return is the redelivery. Held-until-acknowledged plus recover-on-expired-lock is exactly at-least-once: never lost, possibly twice.

the pattern

Hold the job until the acknowledgement and recover a crashed one: at-least-once delivery — never lost, possibly twice. The duplicate is structural, not a defect.

in EchoMQ

moveToActive-11.lua holds the job in emq:{queue}:active under a lock, and moveStalledJobsToWait-8.lua returns an expired-lock job to emq:{queue}:wait — the redelivery that makes the guarantee at-least-once.

EchoMQ holds a job in active until the acknowledgement and returns an expired-lock job to wait; that is at-least-once in real code — never lost, possibly twice.

Door · not depth The full worker fetch loop — the heartbeat manager that renews the lock, the stalled-check coordination across a worker pool, and the polyglot concurrency models — is the dedicated EchoMQ course. From here, R3.02 opens onto E2 · the EchoMQ core — the as-built library and the worker fetch loop. This dive teaches the at-least-once guarantee; that course teaches the engine that ships it.

References

Where the atomic move that holds a job until the acknowledgement is specified, its modern successor, and the queue protocol whose wait/active path EchoMQ ports.

Sources

Related in this course