Module F-3·22 min read

The full data structure surface and how each uses listpack vs ziplist vs skiplist vs hashtable encoding under the hood — what triggers encoding upgrades and how encoding determines memory usage.

Written by Jatin Jain Saraf · Architect of SupraScan

F-3 — Lists, Hashes, Sets, and Sorted Sets

Who this module is for: You know Redis has data structures beyond Strings — you have seen them in documentation — but you have never understood when to reach for each one, why they exist, or what is happening under the hood when you use them. This module teaches the full command surface and internal mechanics of Redis's four most important composite types. By the end, you will not just know the commands; you will know which structure to reach for before you start writing code.


Why Data Structures Matter in Redis

In F-2, you learned that a Redis String is not a string — it is a binary-safe byte array with encoding-aware storage. The same design philosophy applies to every Redis type: the shape of your data and the operations you need should determine which type you use.

This is different from how you might think about SQL. In PostgreSQL, you store everything in tables and columns, then join and aggregate at query time. In Redis, you choose the structure that makes your operations atomic and your memory efficient. The wrong choice does not cause a crash — it causes unnecessary complexity, non-atomic operations, and wasted RAM.

The four structures in this module — Lists, Hashes, Sets, and Sorted Sets — cover the vast majority of Redis use cases. Master these four and you can implement caching, queues, counters, leaderboards, sessions, pub/sub fan-out, social graphs, and rate limiters without reaching for anything else.


Lists

What a Redis List Is

A Redis List is an ordered sequence of strings, linked as a doubly-linked list (or, for small sizes, a listpack — more on this in the encoding section below). You can push elements to either end and pop elements from either end. This makes it both a stack (LIFO) and a queue (FIFO) depending on how you use it.

LPUSH tasks "task-c"   → pushes to the LEFT (head)
LPUSH tasks "task-b"
LPUSH tasks "task-a"
 
State: [task-a, task-b, task-c]   ← head is left
 
RPUSH tasks "task-d"   → pushes to the RIGHT (tail)
 
State: [task-a, task-b, task-c, task-d]

Core Commands

Pushing and popping:

LPUSH  key val [val ...]   → push one or more values to the head, return new length
RPUSH  key val [val ...]   → push one or more values to the tail, return new length
LPOP   key [count]         → pop from the head, return value(s)
RPOP   key [count]         → pop from the tail, return value(s)

Multi-element pop (count argument) was added in Redis 6.2. Before that, you had to call LPOP in a loop.

Inspecting without consuming:

LLEN   key              → number of elements
LRANGE key start stop   → return elements from index start to stop (inclusive)
LINDEX key index        → return element at index (0 = head, -1 = tail)

LRANGE key 0 -1 returns all elements. Redis list indexes are zero-based, and negative indexes count from the tail: -1 is the last element, -2 is the second-to-last, and so on.

127.0.0.1:6379> RPUSH queue "job-1" "job-2" "job-3"
(integer) 3
127.0.0.1:6379> LRANGE queue 0 -1
1) "job-1"
2) "job-2"
3) "job-3"
127.0.0.1:6379> LPOP queue
"job-1"
127.0.0.1:6379> LRANGE queue 0 -1
1) "job-2"
2) "job-3"

Blocking pop — the pattern that powers job queues:

BLPOP key [key ...] timeout   → block until an element is available at the head
BRPOP key [key ...] timeout   → block until an element is available at the tail

BLPOP myqueue 0 blocks the client indefinitely until another client pushes to myqueue. The moment a push occurs, the blocked client is woken and the element is returned. This is how you build a job queue without polling:

# Worker process (blocking, waiting for work)
BLPOP tasks 0       → blocks here
 
# Producer (in another process/connection)
LPUSH tasks "job-99"   → wakes the blocked worker

The timeout argument is in seconds. 0 means block forever (until an element arrives or the connection is closed). A non-zero timeout returns (nil) if nothing arrives within the timeout — useful for workers that need to periodically check for shutdown signals.

BLPOP accepts multiple keys and returns from the first one that has data — this lets a single worker consume from multiple queues with a single connection.

Moving between lists atomically:

LMOVE source destination LEFT|RIGHT LEFT|RIGHT

LMOVE source destination RIGHT LEFT pops from the right of source and pushes to the left of destination atomically. This is the correct way to implement a reliable queue: move a job from pending to processing, and if the worker crashes, the job is still in processing and can be recovered.

The non-atomic alternative — RPOP from one list then LPUSH to another — has a window where the job is in neither list if your application crashes between the two commands.

Trimming a list to a fixed size:

LTRIM key start stop

After LTRIM mylog 0 999, only the first 1,000 elements remain. Use this after every LPUSH to cap a capped log or recent-activity feed.

When to Use a List

  • Job queue / task queue — RPUSH to enqueue, BLPOP to consume (FIFO)
  • Stack — LPUSH to push, LPOP to pop (LIFO)
  • Activity feed / recent events — LPUSH new events, LTRIM to keep the last N, LRANGE to read
  • Reliable queue — LMOVE from pending to processing, acknowledge by deleting from processing

Do not use a List when:

  • You need to look up elements by a specific value (no indexed lookup by content — use a Hash or Set)
  • You need uniqueness (use a Set)
  • You need ordering by score (use a Sorted Set)
  • The list grows to millions of elements and you iterate it frequently (consider Streams)

Hashes

What a Redis Hash Is

A Redis Hash is a map of field names to values — exactly what you might call a dictionary, object, or associative array in your programming language. Both fields and values are strings.

HSET user:1001 name "Jatin" email "j@example.com" role "engineer"
HGET user:1001 name        → "Jatin"
HGET user:1001 email       → "j@example.com"
HGETALL user:1001          → ["name", "Jatin", "email", "j@example.com", "role", "engineer"]

Core Commands

Setting fields:

HSET     key field value [field value ...]   → set one or more fields, return count of new fields
HSETNX   key field value                    → set field only if it does not exist

In Redis 4.0+, HSET accepts multiple field-value pairs. The old HMSET command (which did the same thing) is deprecated. Use HSET.

Getting fields:

HGET    key field                → return the value of field
HMGET   key field [field ...]    → return values of multiple fields (nil for missing)
HGETALL key                      → return all field-value pairs as a flat list
HKEYS   key                      → return all field names
HVALS   key                      → return all values
HLEN    key                      → return the number of fields

HGETALL returns a flat alternating list: field1, value1, field2, value2, ... Client libraries typically convert this to a map for you. For large hashes, be careful with HGETALL — it returns everything in one call and can be slow for hashes with thousands of fields.

Checking and deleting:

HEXISTS key field               → 1 if field exists, 0 if not
HDEL    key field [field ...]   → delete one or more fields, return count deleted

Incrementing numeric fields:

HINCRBY   key field increment   → atomically increment integer field
HINCRBYFLOAT key field increment

This is the right way to track per-user counters inside a Hash object:

HSET user:1001:stats posts 0 comments 0
HINCRBY user:1001:stats posts 1      → (integer) 1
HINCRBY user:1001:stats posts 1      → (integer) 2
HINCRBY user:1001:stats comments 5   → (integer) 5
HGETALL user:1001:stats
1) "posts"
2) "2"
3) "comments"
4) "5"

Scanning large hashes:

HSCAN key cursor [MATCH pattern] [COUNT count]

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.