Redis as a queue
Redis lists make a simple, fast job queue. Here's how to build one and when to reach for something more.
Redis lists support atomic push and pop operations from both ends, which makes them a natural fit for job queues. There is no extra software to install, no broker to configure, and operations run in microseconds. The trade-off is that you have to manage reliability yourself.
The basic pattern
A FIFO queue uses two commands: LPUSH to add work to the left of the list, and RPOP to take work from the right.
RPOP returns and removes the oldest item (the one pushed first). Both operations are O(1). The list key is created automatically on the first LPUSH and deleted automatically when the last item is popped.
Blocking dequeue with BRPOP
Polling with RPOP in a loop wastes CPU cycles when the queue is empty. BRPOP blocks the connection until an item is available, then returns it immediately.
The second argument is a timeout in seconds. If no item arrives within 30 seconds, BRPOP returns a nil reply. A timeout of 0 blocks indefinitely.
BRPOP accepts multiple keys. It returns the first item available, from whichever key has one:
Reliable queue pattern
BRPOP removes the item from the list before your worker processes it. If the worker crashes mid-job, the item is gone.
The reliable pattern moves the item to a separate "processing" list atomically instead of removing it outright. Use RPOPLPUSH (or LMOVE in Redis 6.2+) for this:
The item is now in jobs:processing. When the worker finishes successfully, it removes the item from jobs:processing. If the worker crashes, a recovery process can scan jobs:processing, check for stale items, and requeue them to jobs.
For blocking behavior, use BRPOPLPUSH (deprecated in Redis 6.2, still functional).
Using LMOVE (Redis 6.2+)
LMOVE is the modern replacement for RPOPLPUSH. It gives you explicit control over which end to pop from and which end to push to.
LEFT RIGHT pops from the left of the source and pushes to the right of the destination. The blocking version is BLMOVE:
LMOVE is clearer about intent and works with the same performance characteristics as RPOPLPUSH. Prefer it on Redis 6.2+.
Python worker example
A realistic producer pushes JSON payloads that include a job type, a payload, and a creation timestamp. The consumer blocks on BRPOP in a loop.
Producer:
Consumer:
brpop returns a tuple of (key, value) when an item is available, or None on timeout. Destructuring with _, raw discards the key name.
Priority queues
Run multiple lists and have the worker check the high-priority queue first. BRPOP accepts multiple keys and returns from the first one that has an item:
Redis checks the keys in order. If jobs:high has an item, it is returned first, regardless of what is in jobs:normal or jobs:low. In Python:
Producers push to the appropriate key based on job priority. The worker code stays the same.
Delayed jobs
For scheduled or delayed execution, use a sorted set. Store the job payload as the member and the Unix timestamp for when it should run as the score.
A polling loop checks for jobs that are due:
ZRANGEBYSCORE fetches jobs with a score (due time) at or before now. ZREM removes each one atomically. If two workers race on the same job, only the one that gets a non-zero return from ZREM wins.
When to use a dedicated queue instead
Redis lists are a good queue when:
- Your jobs are short-lived and tolerant of occasional loss
- You have Redis persistence enabled (RDB or AOF) and understand the recovery gap
- You want minimal infrastructure
Redis queues are a poor fit when:
- You need a dead letter queue. Failed jobs have to be handled manually unless you build the retry logic yourself.
- You need message acknowledgment. The reliable pattern approximates this, but it is not built in.
- You need routing or fanout. Lists deliver to one consumer. Routing to multiple queues based on job content requires external logic.
- You cannot tolerate any message loss. A Redis crash between a write and a flush can lose data even with AOF enabled.
Alternatives worth knowing:
| Tool | When to use |
|---|---|
| Redis Streams | Built-in consumer groups, acknowledgment, and replay. Best when you are already on Redis and need more guarantees. |
| BullMQ | TypeScript/Node.js queue library built on Redis. Handles retries, delays, priorities, and dead letter queues for you. |
| Celery + Redis | Python task queue. Mature, well-documented, and works with Redis as the broker. |
| RabbitMQ | Dedicated broker with flexible routing, exchanges, and strong delivery guarantees. |
| Amazon SQS | Managed queue with at-least-once delivery, dead letter support, and no infrastructure to run. |
Quick reference
| Command | Description |
|---|---|
LPUSH key value [value ...] | Add one or more items to the left (tail of queue) |
RPOP key | Remove and return the rightmost item (head of queue) |
BRPOP key [key ...] timeout | Blocking pop; waits until an item is available |
LMOVE source dest LEFT RIGHT | Atomically move an item between lists |
BLMOVE source dest LEFT RIGHT timeout | Blocking version of LMOVE |
RPOPLPUSH source dest | Move item from source to dest (pre-6.2 version of LMOVE) |
LLEN key | Number of items currently in the list |
ZADD key score member | Add a delayed job with a Unix timestamp as the score |
ZRANGEBYSCORE key min max | Fetch jobs due by a given timestamp |
ZREM key member | Remove a job from the sorted set (use return value to avoid races) |