Lists

intermediate redis lists queues data-structures

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

List: tasks
LPUSH ←
task-2
task-1
task-3
→ RPUSH
O(1) push/pop on both ends, O(N) for middle access

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.