Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Redis Lists

Master Redis lists in this hands-on lesson: LPUSH, RPUSH, LPOP, RPOP, LLEN, LRANGE, and more. Learn when to use lists vs sets or streams. Includes a practical exercise and troubleshooting tips for common edge cases. Essential for building queues, logs, and activity feeds.

Focus: work with redis lists

Sponsored

You've successfully stored and retrieved data in Redis, but now you need data structures that maintain order. Strings hold single values; hashes group fields under a key; and sets ensure uniqueness. But when you need a sequence — a message queue, a recent-activity timeline, or a task pipeline — you need Redis lists. Lists let you push and pop elements from both ends with O(1) complexity, making them the workhorse for ordered data in Redis. Let's work with Redis lists and unlock their power.

The problem this lesson solves

Consider building a simple job queue. You have workers that need to process tasks in the order they arrive. Using a database table with timestamps and ORDER BY creates overhead. Without ordering, you'd need complex sorting logic. Even more, what if one worker needs to take the most recent task, while another worker always takes the oldest? Redis lists solve this with operations like LPUSH (left-push) and RPOP (right-pop), or conversely RPUSH and LPOP. The list remains ordered by insertion order, and you control which end to modify.

Pro tip: Redis lists are linked lists — not arrays. This means inserting new elements at the head or tail is O(1), but accessing an element by index is O(n). Keep this in mind for performance.

Core concept / mental model

Think of a Redis list as a double-ended queue (deque). It maintains insertion order. You can: - Push elements to the left (head) or right (tail). - Pop elements from either end. - Get a range of elements without removing them. - Trim the list to a specific range. - Block while waiting for an element to push (blocking pop).

Anatomy of a list

A list is identified by a key. All operations happen on that key. There is no schema — any element is simply a string (binary-safe).

When to use a list

Use Case Example
Queue RPUSH + LPOP (FIFO)
Stack LPUSH + LPOP (LIFO)
Activity feed LPUSH + LRANGE (newest first)
Timeline RPUSH + LRANGE (chronological order)
Log buffer LPUSH + LTRIM (keep last N entries)

How it works step by step

Let's simulate a message queue where producers push messages and workers consume them.

  1. Producer pushes messages to the right (tail) of a list.
  2. Worker pops messages from the left (head) — FIFO order.
  3. If the list is empty, the worker can block until a new message arrives (BRPOP / BLPOP).

Commands at a glance

  • LPUSH key element [element ...] — Add one or more elements to the left.
  • RPUSH key element [element ...] — Add one or more to the right.
  • LPOP key — Remove and return the leftmost element.
  • RPOP key — Remove and return the rightmost element.
  • LRANGE key start stop — Return a range of elements (0-indexed, inclusive).
  • LLEN key — Return the list length.
  • LTRIM key start stop — Trim the list to only the specified range.
  • BLPOP key [key ...] timeout — Blocking left pop (wait up to timeout seconds).
  • BRPOP key [key ...] timeout — Blocking right pop.

Hands-on walkthrough

Let's open your Redis CLI (redis-cli) or run Python with redis-py. We'll build a task queue.

Basic queue operations

# Redis CLI
127.0.0.1:6379> RPUSH queue task1 task2 task3
(integer) 3
127.0.0.1:6379> LLEN queue
(integer) 3
127.0.0.1:6379> LPOP queue
"task1"
127.0.0.1:6379> LRANGE queue 0 -1
1) "task2"
2) "task3"

Working with Python

import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Push tasks
r.rpush('queue', 'task1', 'task2', 'task3')
print(f"Queue length: {r.llen('queue')}")

# Pop tasks
while r.llen('queue') > 0:
    task = r.lpop('queue')
    print(f"Processed: {task}")

print(f"After processing: {r.llen('queue')}")

Expected output:

Queue length: 3
Processed: task1
Processed: task2
Processed: task3
After processing: 0

Stack behavior (LIFO)

# Stack: push left, pop left
r.lpush('stack', 'bottom')
r.lpush('stack', 'middle')
r.lpush('stack', 'top')

print(f"Poppped: {r.lpop('stack')}")  # top
print(f"Poppped: {r.lpop('stack')}")  # middle
print(f"Poppped: {r.lpop('stack')}")  # bottom

Expected output:

Poppped: top
Poppped: middle
Poppped: bottom

Blocking pop for real-time queues

import redis
import threading
import time

def worker():
    r = redis.Redis(decode_responses=True)
    while True:
        # Block until a task arrives (timeout=0 means indefinite wait)
        task = r.blpop('tasks', timeout=0)
        print(f"Worker got: {task}")

# Start worker in background thread
t = threading.Thread(target=worker, daemon=True)
t.start()

time.sleep(0.5)  # Give worker time to wait

r = redis.Redis(decode_responses=True)
r.rpush('tasks', 'urgent_task')
time.sleep(1)
r.rpush('tasks', 'another_task')
time.sleep(2)

Expected output (approximate):

Worker got: ('tasks', 'urgent_task')
Worker got: ('tasks', 'another_task')

Trimming a log buffer

Keep only the last 100 log entries:

# Simulate log entries
for i in range(200):
    r.lpush('logs', f"entry_{i}")
    r.ltrim('logs', 0, 99)  # keep only first 100 (0-99)

print(f"Log count: {r.llen('logs')}")  # 100
# Oldest entry first
print(f"First 5: {r.lrange('logs', -5, -1)}")  

Expected output:

Log count: 100
First 5: ['entry_4', 'entry_3', 'entry_2', 'entry_1', 'entry_0']

Pro tip: LTRIM with negative indices can be tricky. LTRIM key 0 99 keeps first 100 elements (index 0 to 99). If your list grows left (LPUSH), keep indices 0 to N-1.

Compare options / when to choose what

Structure Ordering Unique? Use case
List Insertion order No Queues, stacks, timelines
Set Unordered Yes Tags, graph nodes
Sorted Set By score Yes Leaderboards, ranges by score
Stream Ordered by auto-generated ID No, but consumer groups Event sourcing, logging at scale

When lists outperform alternatives

  • Simple queue: Lists with blocking pop (BLPOP/BRPOP) are the simplest way to build a message queue in Redis. No consumer groups needed.
  • Recent items timeline: Display last 50 comments — LPUSH + LTRIM + LRANGE is O(1) for push/trim and O(50) for range.
  • Stack: LIFO behavior is native to lists (LPUSH + LPOP).

When to avoid lists

  • Unique members: Use sets or sorted sets.
  • Range queries by value: Sorted sets allow score-based ranges.
  • Persistence/crash safety: Lists are memory-bound; if Redis crashes, data can be lost (unless replication/AOF). For critical message durability, use Redis Streams.

Troubleshooting & edge cases

Situation Symptom Fix
Empty list LPOP returns nil Check length before popping; use BLPOP with timeout
Negative index confusion Wrong range LRANGE queue 0 -1 from left to right; -1 is the last element
Memory bloat List grows unbounded Use LTRIM after LPUSH/RPUSH to cap size
Wrong pop direction FIFO vs LIFO mixup Remember: push right + pop left = FIFO; push left + pop left = LIFO
Blocking pop never returns Worker hangs forever Ensure at least one timeout is set; check network connectivity to Redis
String type mismatch WRONGTYPE Operation against a key holding the wrong kind of value Check key type with TYPE key; use DEL key if needed

Common mistake: mixing left and right

# Wrong: push left, pop right for FIFO
redis.r.lpush('queue', 'item1')  # 'item1' at left
redis.r.rpop('queue')            # pops from right (last) -> 'item1' becomes 'item2'? No!

Fix: Use rpush + lpop for FIFO, or lpush + rpop.

Edge case: huge list

LRANGE on a list with 1 million elements is O(N). If you need to process all, use LTRIM + loop:

batch_size = 100
while redis.llen('big_list') > 0:
    batch = redis.lrange('big_list', 0, batch_size - 1)
    redis.ltrim('big_list', batch_size, -1)
    # Process batch

What you learned & what's next

You now understand how to work with Redis lists: - You can create queues (FIFO) and stacks (LIFO). - You can trim lists to bounded size. - You can block on empty lists for real-time processing. - You know when to choose lists over sets, sorted sets, or streams.

This is a foundational skill for building notification queues, activity feeds, and log aggregators. In the next lesson, we'll explore Redis streams — a more advanced structure for event sourcing and consumer groups.

Remember: Lists are simple but powerful. Master them to build robust, fast ordering in your applications.

Practice recap

Try building a mini task queue: In Python, create two functions: producer() that pushes 10 random tasks to a list, and worker() that pops them with a 1-second sleep between each. Add a timeout to the blocking pop so the worker exits if idle for 5 seconds. Print each task as it's processed. This simulates a real-world job dispatcher.

Common mistakes

  • Mixing left and right: For a FIFO queue, use RPUSH + LPOP (or LPUSH + RPOP). For a stack, use LPUSH + LPOP (or RPUSH + RPOP).
  • Not trimming lists: Lists can grow unbounded. Always call LTRIM after LPUSH/RPUSH if you need to cap size.
  • Using LRANGE for the entire list without limit: On large lists, LRANGE 0 -1 is O(N). Use LTRIM with a small range instead.
  • Assuming lists are sorted by value: Lists maintain insertion order, not value order. Use sorted sets for that.
  • Forgetting to decode responses: In Python, set decode_responses=True to get strings instead of bytes.

Variations

  1. Use LPUSH + BRPOP for a queue with prioritized processing (worker waits for new items).
  2. Use BLPOP + LPUSH for a stack with blocking (e.g., job assignment workers).
  3. Combine lists with PUBLISH/SUBSCRIBE: push a message to a list and notify workers via pub/sub.

Real-world use cases

  • Background job queue: Producers RPUSH tasks; workers LPOP and process. Use BRPOP for idle workers.
  • Recent activity timeline: LPUSH new events, LTRIM to keep last 100, LRANGE to display.
  • Chat message buffer: Each chat room has a list; messages are RPUSHed and clients LRANGE periodically.

Key takeaways

  • Redis lists are linked lists that support O(1) push/pop from both ends.
  • Use RPUSH + LPOP for FIFO queues; LPUSH + LPOP for LIFO stacks.
  • BLPOP and BRPOP block until an element is available — ideal for worker pools.
  • LTRIM keeps your list bounded and memory-constrained.
  • Lists maintain insertion order, not value order — use sorted sets for score-based ordering.
  • Avoid LRANGE on huge lists; process in batches with LTRIM.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.