Redis Lists and Simple Queues
- redis
- database
- lists
- queues
- backend
- data-structures
A Redis list is an ordered sequence of strings you push and pop from either end. That simple shape makes it the natural fit for two common needs: a queue where producers add work and consumers take it, and a capped feed of recent items. Lists aren't the answer to every queuing problem (streams handle the demanding cases), but for straightforward FIFO work and "latest N" feeds, they're the quick, correct tool.
This is part 5 of the Redis Masterclass, following hashes.
Pushing and popping
Lists have operations for both ends, left and right:
LPUSH queue:emails "job1" # add to the head (left)
RPUSH queue:emails "job2" # add to the tail (right)
LPOP queue:emails # remove and return from the head
RPOP queue:emails # remove and return from the tail
LLEN queue:emails # length
LRANGE queue:emails 0 -1 # all elements
Combine a push on one end with a pop on the other and you get a queue. LPUSH plus RPOP is first-in-first-out: the oldest item comes out. LPUSH plus LPOP is last-in-first-out, a stack. Every one of these operations is atomic, so many producers and consumers can hit the same list safely.
A simple work queue
The basic job queue is producers LPUSH-ing work and workers RPOP-ing it:
# producer
LPUSH jobs:email '{"to":"a@b.com","template":"welcome"}'
# worker
RPOP jobs:email # get the next job, or nil if empty
The problem with plain RPOP is that an empty queue returns nil immediately, so a worker would have to poll in a loop, burning CPU and adding latency. The fix is the blocking variant:
BRPOP jobs:email 5 # wait up to 5 seconds for a job, then return
BRPOP blocks until an item is available (or the timeout elapses), so workers sleep efficiently and wake the instant a job arrives. This turns a list into a real push-based work queue with no polling.
The reliability gap
There's an honest limitation to know before using a list as a job queue. With RPOP or BRPOP, the moment a worker pops a job, it's gone from the list. If that worker crashes while processing, the job is lost, because it's neither in the queue nor completed. Plain lists give at-most-once delivery, which is fine for work you can afford to drop and wrong for work you can't.
The classic mitigation is LMOVE (or the older RPOPLPUSH), which atomically pops from the queue and pushes onto a "processing" list in one step:
LMOVE jobs:email jobs:processing RIGHT LEFT
Now the job is on jobs:processing while the worker handles it. On success the worker removes it from there; on crash it's still on the processing list and can be recovered. This reliable-queue pattern closes the loss gap, but it's manual bookkeeping. When you need real delivery guarantees, consumer groups, and acknowledgements, Redis streams (later in the series) are built for exactly that, and are usually the better choice for important work. Use lists for simple or best-effort queues, streams for reliable ones.
Capped feeds: the "latest N" pattern
The other great use of lists is a bounded list of recent items: the last 10 notifications, recent activity, a timeline. You LPUSH new items on the front and trim the list to a fixed length so it never grows unbounded:
LPUSH user:1:notifications "You have a new follower"
LTRIM user:1:notifications 0 9 # keep only the newest 10
LTRIM keeps the specified range and discards the rest. Pushing on the front and trimming to the first N gives you a self-maintaining "most recent N" list that's cheap to read with LRANGE user:1:notifications 0 -1. This is a clean pattern for activity feeds and recent-items widgets where you only ever show the latest handful.
When a list is and isn't the right queue
To keep the choice clear:
- Use a list for simple FIFO/LIFO queues, best-effort work you can afford to lose, and capped recent-item feeds. It's minimal and fast.
- Use
LMOVEto a processing list when you need basic crash recovery but not full guarantees. - Use streams when you need reliable delivery, acknowledgements, multiple consumer groups, or replay. Lists weren't built for those, and forcing them leads to fragile custom bookkeeping.
Lists are the straightforward ordered-sequence structure: push and pop from either end, block to wait for work, trim to cap length. For simple queues and recent-item feeds they're exactly right, and knowing their reliability limit tells you when to graduate to streams instead.
Next, we cover sets: unordered collections of unique values with fast membership tests and set algebra done inside Redis.
Key takeaways
- A Redis list is an ordered sequence you push and pop from either end; combining opposite ends gives a FIFO queue.
- Use `BRPOP` (blocking pop) so workers wait efficiently for jobs instead of polling an empty queue.
- Plain list queues are at-most-once: a job popped by a worker that then crashes is lost.
- `LMOVE` to a processing list adds basic crash recovery; for real delivery guarantees, use streams instead.
- `LPUSH` plus `LTRIM` maintains a capped "latest N" feed cheaply, ideal for notifications and recent activity.
Frequently asked questions
How do I build a queue with a Redis list?
Producers `LPUSH` items onto the list and workers `RPOP` (or `BRPOP`) from the other end for FIFO order. Use the blocking `BRPOP` so workers wait for new jobs efficiently rather than polling an empty list.
What's the problem with using a list as a job queue?
Plain `RPOP`/`BRPOP` removes the job immediately, so if the worker crashes mid-processing, the job is lost (at-most-once delivery). Use `LMOVE` to a processing list for recovery, or use Redis streams for real delivery guarantees.
What does BRPOP do?
It's a blocking pop: it waits until an item is available on the list or a timeout elapses, then returns it. This lets workers sleep until work arrives instead of repeatedly polling, giving a push-style queue.
How do I keep only the latest N items in a list?
Push new items with `LPUSH` and then run `LTRIM key 0 N-1` to keep only the newest N, discarding the rest. This maintains a self-capping recent-items feed cheaply.
Should I use a list or a stream for queuing?
Use a list for simple or best-effort queues and recent-item feeds. Use a stream when you need reliable delivery, acknowledgements, consumer groups, or replay, which lists don't provide without fragile manual bookkeeping.
Further reading
Explore more on

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.