A Redis list is an ordered collection of strings, implemented internally as a quicklist (linked list of listpacks). Push/pop on either end is O(1). That O(1) at both ends is what makes lists perfect for queues.
Think of it like a JavaScript Array where push, unshift, pop, and shift are all guaranteed fast no matter how many items are inside.
Push and pop
LPUSH tasks "task-1" # left = head
LPUSH tasks "task-2"
RPUSH tasks "task-3" # right = tail
LRANGE tasks 0 -1
# 1) "task-2"
# 2) "task-1"
# 3) "task-3"
LPOP tasks
# "task-2"
RPOP tasks
# "task-3"
The mnemonic: L is left/head, R is right/tail. Pushing on the left grows the head; ranging from 0 gives us the most recent first.
Layout
The job queue pattern
This is the killer use case. A producer pushes jobs; a worker pops them. BLPOP blocks the worker until something arrives — no busy polling.
# Producer
LPUSH queue:emails "user42@example.com"
LPUSH queue:emails "user43@example.com"
# Worker (blocks up to 0 seconds = forever)
BRPOP queue:emails 0
# 1) "queue:emails"
# 2) "user43@example.com"
LPUSH + BRPOP gives us FIFO (first in, first out). Producer pushes to the head, worker pops from the tail.
Reliable queue: BLMOVE
Naive BRPOP has a flaw — if the worker crashes after popping but before processing, the job is lost. BLMOVE (and the older BRPOPLPUSH) atomically moves the job to a “processing” list:
BLMOVE queue:emails queue:processing RIGHT LEFT 0
# work the job...
LREM queue:processing 1 "user43@example.com"
If the worker crashes, a recovery process can re-queue items stuck in queue:processing.
Capped lists for logs
Want the most recent 100 events and nothing more? LPUSH + LTRIM:
LPUSH events:user:42 "logged_in@2026-05-26T10:15"
LTRIM events:user:42 0 99 # keep only indices 0..99
LLEN events:user:42
# (integer) 100 (max)
LTRIM is O(N) but N is bounded by the trim window. This pattern is how Redis is often used for “last N items” feeds.
Inspect and mutate
LRANGE tasks 0 -1 # all elements
LINDEX tasks 0 # element at index 0
LSET tasks 0 "task-updated"
LLEN tasks
LREM tasks 1 "task-1" # remove first occurrence of "task-1"
LRANGE 0 -1 is the Redis equivalent of “give me everything” — -1 means the last element. Watch out on huge lists; this is O(N).
Client usage
// BullMQ and similar libraries build on top of this exact pattern.
await redis.lpush("queue:emails", JSON.stringify({ to, subject, body }));
while (true) {
const [, raw] = await redis.brpop("queue:emails", 0);
const job = JSON.parse(raw);
await sendEmail(job);
}
When lists are wrong
If we need uniqueness, use a set. If we need ordering by score (not insertion order), use a sorted set. If we need a Kafka-like log with multiple consumer groups and replay, use streams — they’re purpose-built for that and lists fall apart at scale there.