R3.01.2 · dive 2
LMOVE / RPOPLPUSH
Pop, then push, is two commands with a gap in the middle. One command — RPOPLPUSH, or the modern LMOVE — closes the gap.
Moving a job from the waiting list to the in-flight list is one logical step, so it must be one
Redis command. Done as two — RPOP, then LPUSH — it
has a window in the middle where the job is in neither list, and a crash there drops it.
RPOPLPUSH and its successor LMOVE do the pop and the push as
one atomic command. Toggle the move on the right between two steps and one, then crash at the gap and watch only
the two-step form lose the job.
Two steps vs one · crash at the gap, job 7
Two steps leave a gap
The obvious way to take a job is to pop it from the waiting list and push it to the in-flight list. Between the
RPOP and the LPUSH, the job exists nowhere: it has left
wait and has not yet reached active. A crash in that window
— a deploy, an OOM kill, a dropped connection — drops the job with nothing to recover it from.
# two commands: a gap in the middle job = RPOP wait # job leaves wait — it is now in neither list # ---- if the worker dies HERE, the job is gone ---- LPUSH active job # job lands in active
The window is small, but a busy queue runs the move millions of times, and small windows fire. Two commands cannot be made atomic by running them quickly; the only fix is to make the move a single command.
Pop-then-push is two commands with a gap in which the job is in neither list; a crash there loses it, and running faster cannot close the gap.
One command closes it
RPOPLPUSH source destination pops from the tail of source and
pushes to the head of destination as a single atomic command. The job leaves
wait and lands in active with no instant in between in which
it is in neither list.
job = RPOPLPUSH wait active # atomic: pop tail of wait, push head of active process(job) # a crash here leaves the job parked in active LREM active 1 job # remove only after the work is truly done
LMOVE source destination RIGHT LEFT (Redis 6.2+) is the same move with explicit
directions. RPOPLPUSH src dst is exactly
LMOVE src dst RIGHT LEFT — pop the right (tail), push the left (head).
LMOVE generalises it to any pair of ends and is the recommended form for new code;
RPOPLPUSH remains for compatibility and is what older protocols, EchoMQ included, run.
Either way the guarantee is identical: the job is in exactly one list at every instant.
RPOPLPUSH src dst == LMOVE src dst RIGHT LEFT LMOVE src dst LEFT RIGHT # pop the head of src, push the tail of dst
- RPOPLPUSH
- Pop the tail of
source, push the head ofdestination, atomically. The original move, fixed directions. - LMOVE
- The Redis 6.2+ successor with explicit
RIGHT/LEFTends;RPOPLPUSH src dst==LMOVE src dst RIGHT LEFT. The recommended form for new code.
One command — RPOPLPUSH, or LMOVE — does
the pop and the push atomically, so there is no instant at which the job exists nowhere.
The blocking variant — block, do not poll
A worker with nothing to do can poll: run RPOPLPUSH in a loop, get nil on an empty
queue, sleep, try again. Polling wastes round trips when the queue is idle and adds latency between a job
arriving and a worker taking it. The blocking variants remove the loop.
BLMOVE source destination RIGHT LEFT timeout (and the older
BRPOPLPUSH source destination timeout) do the same atomic move, but on an empty source
they block the connection for up to timeout seconds, waking the instant a job is
pushed.
BRPOPLPUSH wait active 30 # block up to 30s; wake the moment a job arrives BLMOVE wait active RIGHT LEFT 30 # the modern form, same blocking move
A timeout of 0 blocks indefinitely. The block is server-side: no round trips while waiting, and the worker takes the job with no polling delay. The move is still atomic — blocking changes only when the command returns, not what it does. Slide the queue's idle gap below and compare the round trips a poll loop makes against the single blocked call.
Poll vs block · round trips over an idle gap
A poll loop makes one wasted round trip per idle second; a blocking BLMOVE
makes one call and waits, the same atomic move with no polling delay.
In EchoMQ — the move is one command
EchoMQ's in-flight move is one command, not two, in real code in echo/apps/echomq.
EchoMQ.Scripts.move_to_active/4 dispatches
moveToActive-11.lua, whose move is
rcall("RPOPLPUSH", waitKey, activeKey) — the
RPOPLPUSH form, inside a larger Lua script that also sets the lock and writes an event,
the whole thing atomic. The job leaves emq:{queue}:wait and lands in
emq:{queue}:active with no gap.
-- moveToActive-11.lua — the move is RPOPLPUSH, not RPOP + LPUSH (real) local waitKey = KEYS[1] -- emq:{queue}:wait local activeKey = KEYS[2] -- emq:{queue}:active local jobId = rcall("RPOPLPUSH", waitKey, activeKey) -- one command, atomic
EchoMQ runs the move inside a Lua script rather than as a bare BLMOVE, because the move
also has to set the job's lock and write to the event stream — work that has to be atomic with the
pop-and-push, so it goes in one script. The bare RPOPLPUSH and
BLMOVE are the same closing-the-gap move at the command level; the script is that move
plus the lock and the event, all-or-nothing.
The move from waiting to in-flight is one logical step and must be one Redis command;
RPOPLPUSH / LMOVE closes the gap a pop-then-push leaves.
move_to_active/4 runs moveToActive-11.lua, whose move is
rcall("RPOPLPUSH", waitKey, activeKey) — one command, inside one atomic script
that also sets the lock.
Two commands leave a gap; one command closes it. EchoMQ's move is the single
RPOPLPUSH inside moveToActive-11.lua.
BLMOVE — the lock it sets, the
event it writes, the rate-limit and delayed-set branches it folds in, the eleven keys it touches — is the
dedicated EchoMQ course. From here, this dive opens onto
E2 · the engine — the as-built moveToActive include
graph. This dive teaches the atomic move; that course teaches the whole script.
References
The atomic move, its modern successor, and the blocking variant that lets a worker consume without polling.
Sources
- Redis — RPOPLPUSH — the original atomic move: pop the tail of source, push the head of destination, in one command.
- Redis — LMOVE — the modern successor (Redis 6.2+), explicit
RIGHT/LEFT, recommended for new code. - Redis — BLMOVE — the blocking variant: the same atomic move, block on an empty source instead of polling.
- Salvatore Sanfilippo — RPOPLPUSH, the reliable queue pattern — the Redis creator's design note on the atomic move.
Related in this course
- R3.01 · Processing list — the module hub: the in-flight move.
- R3.01.1 · List as wait + active — the two named lists the command moves a job between.
- R3.01.3 · The in-flight list — what the atomic move enables: a recoverable parked job.
- R2.01 · Atomic updates — the broader atomic read-modify-write family this move belongs to.
- E2 · the engine — the dedicated EchoMQ course: the worker fetch loop in depth.