LPOP and RPOP
LPOP and RPOP are the two Redis commands you reach for whenever you need to remove an element from a list. LPOP removes from the left end (the head), and RPOP removes from the right end (the tail). Paired with LPUSH and RPUSH, they turn a Redis list into a stack, a FIFO queue, or a double-ended queue depending on which ends you push and pop from. Both commands run in O(1) time for a single element, execute atomically because Redis is single-threaded, and — importantly — delete the key entirely once the last element is popped.
Overview / How it works
A Redis list is an ordered sequence of string values, and internally Redis stores it as one or more listpacks — compact, contiguous-memory encoded blocks — chained together into a quicklist once the list grows large enough. For small lists, Redis keeps everything in a single listpack. Both representations are optimized for cheap operations at the two ends: adding or removing the first or last element doesn’t require shifting every other element in memory, which is why LPOP and RPOP are O(1) per element popped, regardless of how long the list is. Contrast this with something like LINDEX at an arbitrary offset, which has to walk the list from the nearest end.
Because Redis executes one command at a time on its single main thread, a call to LPOP or RPOP is atomic from start to finish — no other client’s command can interleave in the middle of it. That’s what makes lists safe to use as work queues shared between multiple producers and consumers without any extra locking: two workers calling LPOP on the same queue will always receive two different elements, never the same one twice.
Since Redis 6.2, both commands accept an optional count argument, letting you pop several elements from the same end in a single round trip instead of looping one pop at a time. When you pop with a count from the left, elements come out in head-to-tail order; when you pop with a count from the right, elements come out in tail-to-head order (i.e., the very last element first).
When the last element is removed from a list — by either command — Redis deletes the key from the keyspace immediately. There is no such thing as an empty list key sitting around; EXISTS on that key will report 0 right after the final pop, and any TTL you had set on it is gone along with the key.
If the list is empty or the key doesn’t exist, both commands return (nil) rather than an error — an empty result is a completely normal, expected reply, not a failure condition. If the key exists but holds a different data type (a string, a hash, a set), Redis refuses the operation with a WRONGTYPE error, since every key has exactly one type.
Syntax
LPOP key [count]
RPOP key [count]
key— the name of the list. Required.count— optional non-negative integer specifying how many elements to pop. Omit it to pop exactly one element and get back a single bulk string (or(nil)). Supply it to get back an array of that many elements (or(nil)if the key doesn’t exist, or fewer elements than requested if the list is shorter).
| Command | Time Complexity |
|---|---|
LPOP key |
O(1) |
LPOP key count |
O(N), where N is the number of elements returned |
RPOP key |
O(1) |
RPOP key count |
O(N), where N is the number of elements returned |
Examples
Example 1: A simple FIFO queue with RPUSH and LPOP
Push three tasks onto a queue with RPUSH, then pull the oldest one off the front with LPOP:
RPUSH tasks:queue "task1" "task2" "task3"
LPOP tasks:queue
LRANGE tasks:queue 0 -1
Output:
(integer) 3
"task1"
1) "task2"
2) "task3"
RPUSH appends the three tasks to the tail, in order, so the list from head to tail is task1, task2, task3. LPOP removes from the head, returning "task1" — the first task that was ever pushed. Combining RPUSH (add to tail) with LPOP (remove from head) gives you first-in-first-out ordering, exactly what you want for a work queue.
Example 2: Popping several elements at once with COUNT
Use the count argument to drain multiple elements from the tail in one call instead of looping:
RPUSH playlist:1 "song_a" "song_b" "song_c" "song_d"
RPOP playlist:1 2
LRANGE playlist:1 0 -1
Output:
(integer) 4
1) "song_d"
2) "song_c"
1) "song_a"
2) "song_b"
The list holds song_a, song_b, song_c, song_d from head to tail. RPOP playlist:1 2 pops two elements from the tail, and returns them in the order they were popped — song_d first, then song_c — not in their original list order. The remaining list, checked with LRANGE, is just song_a and song_b.
Example 3: Using LPUSH and LPOP together as a LIFO stack
An undo stack for a document editor is a natural fit for a stack: the most recently recorded action should be the first one undone.
LPUSH doc:42:undo "bold" "italic" "underline"
LPOP doc:42:undo
LPOP doc:42:undo
Output:
(integer) 3
"underline"
"italic"
LPUSH inserts each argument at the head one at a time, so after pushing bold, then italic, then underline, the head-to-tail order is underline, italic, bold — the most recently pushed action sits at the head. Because LPOP also removes from the head, the two commands together give last-in-first-out order: the first LPOP undoes underline (the most recent action), and the second undoes italic.
Example 4: Popping from a list that doesn’t exist
Both commands treat a missing key the same way whether or not you supply a count:
LPOP session:999:actions
LPOP session:999:actions 3
Output:
(nil)
(nil)
Neither call is an error. A missing key is treated as an empty list, so both the single-element form and the count form simply reply (nil). Your application code should treat a (nil) reply from LPOP/RPOP as “nothing to process right now,” not as a failure.
How it works step by step
When Redis receives an LPOP key (the logic for RPOP is identical, mirrored to the other end):
- Redis looks up
keyin the keyspace dictionary. If it isn’t found, it replies(nil)immediately, whether or not a count was supplied. - If the key exists, Redis checks its type. If it isn’t a list, it replies with a
WRONGTYPEerror and stops without touching anything. - Otherwise, Redis detaches the value (or the first
countvalues) from the head of the underlying listpack/quicklist structure. This is a pointer/length adjustment at the edge of the structure, not a scan or a copy of the remaining elements — hence O(1) per element. - If that removal empties the list completely, Redis deletes the key from the keyspace right away, along with any TTL that was attached to it.
- Redis sends the popped value (or array of values) back to the client as the reply.
You can see step 4 directly:
RPUSH cache:job:77 "step1"
LPOP cache:job:77
EXISTS cache:job:77
Output:
(integer) 1
"step1"
(integer) 0
After the single element is popped, EXISTS reports 0 — the key is simply gone, not “a list with zero elements.”
Common Mistakes
Mistake 1: Calling LPOP/RPOP on the wrong data type
Every Redis key has exactly one type. If you accidentally created a string at a key you meant to use as a list, popping from it fails:
SET user:500:profile "alice"
LPOP user:500:profile
Output:
OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value
The fix is to use a different key name for the list, or to DEL the existing key first if it was created by mistake and you don’t need its current value, then build it correctly with RPUSH instead.
Mistake 2: Passing a negative count
count must be zero or positive — a negative value is rejected outright rather than being interpreted as “pop from the other end” or similar:
RPUSH cart:88:items "sku_1" "sku_2"
LPOP cart:88:items -1
Output:
(integer) 2
(error) ERR value is out of range, must be positive
If you want “pop everything,” pass the list’s actual length (from LLEN) as the count, or just omit the count and loop until you get (nil) back.
Mistake 3: Assuming an empty list key still exists
As shown above, Redis deletes a list key the moment its last element is popped. A common bug is code that sets a TTL on a queue key once, assuming it’ll persist across refills — but if the queue is ever fully drained, the key (and its TTL) disappear, so the next RPUSH creates a brand-new key with no TTL at all. If your application depends on a TTL always being present, re-apply it (e.g. with EXPIRE) after every RPUSH that might be creating the key fresh, not just the first time.
Mistake 4: Checking LLEN before popping instead of just popping
Calling LLEN to check “is there anything to pop?” and then calling LPOP in a second round trip looks safe but isn’t: another client can pop the same element between your two calls, since each command is atomic individually but the two calls together are not. Skip the check entirely — call LPOP directly and treat a (nil) reply as “nothing was there,” which is both simpler and race-free.
Best Practices
- Pick your push/pop ends deliberately:
RPUSH+LPOPis a FIFO queue;LPUSH+LPOP(orRPUSH+RPOP) is a LIFO stack. Be consistent within a given key. - Use the
countargument to drain several elements in one round trip when a worker can process a small batch at once, instead of looping single pops. - Treat a
(nil)reply as the normal “empty” case, not an error to catch and log. - Don’t pair
LLENwith a separateLPOP/RPOPto “check before acting” — just pop and check the result. - If a consumer needs to wait for work to arrive rather than poll, reach for the blocking variants
BLPOP/BRPOPinstead of callingLPOPin a tight loop, which wastes CPU cycles on Redis’s single thread for every idle poll. - Remember that a fully-drained list key is deleted along with its TTL — reset any TTL you rely on after refilling a queue that might have been emptied.
Practice Exercises
- Build a “recent searches” stack at
search:history:1001: push three search terms of your choice withLPUSH, then pop the single most recent one withLPOPand confirm withLRANGEthat the other two remain in the right order. - Simulate a print queue at
printer:queue: useRPUSHto add four job names, then use oneLPOPcall with a count to process the first two jobs at once, and check what’s left withLRANGE. - Create a key with
SET report:daily "pending", then try toLPOPit. Predict the error you’ll see before running it, then verify, and figure out what command you’d need to run first if you actually wantedreport:dailyto be a list.
Summary
LPOPremoves from the head (left) of a list;RPOPremoves from the tail (right).- Both are O(1) for a single element, and O(N) for N elements when a
countis given. - An optional
countargument (Redis 6.2+) pops multiple elements in one call; elements come back in the order they were removed. - A missing key or empty list returns
(nil), never an error — an error means the key holds the wrong type or an argument was invalid. - Popping the last element deletes the key entirely, including any TTL that was set on it.
- Both commands run atomically thanks to Redis’s single-threaded execution model, making lists safe as shared queues across multiple clients without extra locking.
- Avoid check-then-act patterns like
LLENfollowed byLPOP; just pop and inspect the result.
