Guarantee at-least-once message delivery using LMOVE to atomically transfer messages to a processing list, enabling recovery if consumers crash before completing work.
Standard queue operations using RPOP are unreliable—if a consumer fetches a message and crashes before processing completes, the message is lost forever. This pattern ensures no messages are lost.
A naive queue implementation:
LPUSH queue "message"RPOP queue — message is removedInstead of removing the message, atomically move it to a "processing" list. The message remains in Redis until explicitly deleted after successful processing.
The consumer uses LMOVE (or the older RPOPLPUSH) to atomically transfer a message from the main queue to a processing queue:
LMOVE work_queue processing:worker1 RIGHT LEFT
This single command:
- Removes the message from the right of work_queue
- Adds it to the left of processing:worker1
- Does both atomically—no message loss possible
After successful processing, remove the message from the processing queue:
LREM processing:worker1 1 "message"
LMOVE work_queue processing:worker1 RIGHT LEFTLREM processing:worker1 1 "message"If the worker crashes between steps 1 and 3, the message remains in processing:worker1.
A separate monitor process (sometimes called a "reaper") periodically scans processing queues for stalled messages:
LMOVE processing:worker1 work_queue RIGHT RIGHTThis guarantees "at-least-once" delivery—messages may be processed multiple times but are never lost.
For efficient consumption without polling, use the blocking version:
BLMOVE work_queue processing:worker1 RIGHT LEFT 30
This waits up to 30 seconds for a message to arrive. If the queue is empty, the connection blocks rather than returning immediately.
Each worker should have its own processing queue (e.g., processing:worker1, processing:worker2). This allows the monitor to identify which worker holds which messages and makes recovery straightforward.
This pattern provides at-least-once delivery:
For exactly-once semantics, make message handlers idempotent—processing the same message twice should produce the same result as processing it once.
For more complex requirements (multiple consumers per message, message replay, consumer groups), consider Redis Streams instead, which provide these features natively.