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):

  1. Redis looks up key in the keyspace dictionary. If it isn’t found, it replies (nil) immediately, whether or not a count was supplied.
  2. If the key exists, Redis checks its type. If it isn’t a list, it replies with a WRONGTYPE error and stops without touching anything.
  3. Otherwise, Redis detaches the value (or the first count values) 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.
  4. 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.
  5. 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 + LPOP is a FIFO queue; LPUSH + LPOP (or RPUSH + RPOP) is a LIFO stack. Be consistent within a given key.
  • Use the count argument 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 LLEN with a separate LPOP/RPOP to “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/BRPOP instead of calling LPOP in 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

  1. Build a “recent searches” stack at search:history:1001: push three search terms of your choice with LPUSH, then pop the single most recent one with LPOP and confirm with LRANGE that the other two remain in the right order.
  2. Simulate a print queue at printer:queue: use RPUSH to add four job names, then use one LPOP call with a count to process the first two jobs at once, and check what’s left with LRANGE.
  3. Create a key with SET report:daily "pending", then try to LPOP it. 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 wanted report:daily to be a list.

Summary

  • LPOP removes from the head (left) of a list; RPOP removes from the tail (right).
  • Both are O(1) for a single element, and O(N) for N elements when a count is given.
  • An optional count argument (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 LLEN followed by LPOP; just pop and inspect the result.