Redis Data Structures
Why Data Structures Matter
Redis is not just a key-value cache. Each data structure is optimized for specific access patterns and uses different internal encodings based on size. Choosing the right type avoids awkward workarounds, reduces memory, and improves performance.
Wrong choice example: storing a user object as a JSON string when a hash allows field-level updates without rewriting the entire blob.
Internal Encodings (Brief)
Redis automatically selects compact encodings for small values:
| Structure | Small Value Encoding | Large Value Encoding |
|---|---|---|
| String | embstr (embedded) | raw (SDS) |
| Hash | listpack (≤512 fields, small values) | hash table |
| List | listpack | linked list + listpack |
| Set | listpack (integers) or hash table | hash table |
| ZSET | listpack | skip list + hash table |
Use OBJECT ENCODING key to inspect:
SET small "hello"
OBJECT ENCODING small
# "embstr"
HSET user:1 name "Alice" email "[email protected]"
OBJECT ENCODING user:1
# "listpack" or "hash"
Strings
Binary-safe strings up to 512 MB. Used for counters, flags, serialized JSON, and bitmaps.
SET user:1001:name "Alice"
GET user:1001:name
INCR page:views
INCRBY counter:downloads 5
DECRBY inventory:sku:99 3
SETEX session:abc 3600 "token_data"
SETNX lock:resource "owner-1"
APPEND log:events "new entry\n"
STRLEN log:events
Use when: simple scalar values, atomic counters, or serialized objects read/written as a whole.
Hashes
Field-value maps ideal for objects with named properties.
HSET user:1001 name "Alice" email "[email protected]" age 30
HGET user:1001 email
HGETALL user:1001
HMGET user:1001 name email
HINCRBY user:1001 login_count 1
HEXISTS user:1001 phone
HDEL user:1001 temp_field
HLEN user:1001
Hashes are memory-efficient for small objects compared to storing JSON strings — Redis packs small hashes in listpack encoding.
Use when: objects with fields updated independently (sessions, user profiles, product attributes).
Lists
Ordered collections — doubly linked lists supporting push/pop from both ends.
LPUSH notifications:user:1001 "msg1" "msg2"
RPUSH notifications:user:1001 "msg3"
LRANGE notifications:user:1001 0 9
LPOP notifications:user:1001
BRPOP job:queue 30
LTRIM recent:searches 0 49
LLEN recent:searches
LINDEX recent:searches 0
Use when: queues, stacks, recent activity feeds, job processing.
Sets
Unordered collections of unique members.
SADD tags:article:42 redis caching performance
SMEMBERS tags:article:42
SISMEMBER tags:article:42 redis
SCARD tags:article:42
SINTER tags:article:42 tags:article:43
SUNION followers:user:1 followers:user:2
SDIFF followers:user:1 followers:user:2
SPOP tags:article:42 1
SRANDMEMBER tags:article:42
Use when: unique tags, follower relationships, permission sets, deduplication.
Sorted Sets (ZSET)
Sets with an associated score for ordering.
ZADD leaderboard 1500 "player1" 2200 "player2" 1800 "player3"
ZRANGE leaderboard 0 9 WITHSCORES
ZREVRANGE leaderboard 0 2 WITHSCORES
ZREVRANK leaderboard "player2"
ZINCRBY leaderboard 100 "player1"
ZRANGEBYSCORE leaderboard 1000 2000 WITHSCORES
ZCOUNT leaderboard 1000 2000
ZREMRANGEBYRANK leaderboard 0 -101
Use when: leaderboards, priority queues, time-series with scores as timestamps.
Streams
Append-only log for event sourcing and durable messaging.
XADD events * action "login" user_id 1001
XADD events * action "purchase" user_id 1001 amount 49.99
XREAD COUNT 10 STREAMS events 0
XRANGE events - +
XLEN events
XTRIM events MAXLEN ~ 10000
Consumer groups (covered in Advanced Types) provide at-least-once delivery across workers.
Use when: event logs, audit trails, message queues requiring replay.
Choosing a Structure
| Structure | Best For | Avoid When |
|---|---|---|
| String | Counters, simple values, JSON blobs | Frequent partial updates |
| Hash | Object properties | Very large objects (> thousands of fields) |
| List | Queues, recent items | Random access by index at scale |
| Set | Unique tags, relationships | Need ordering |
| ZSET | Rankings, time-series scores | Only need uniqueness, not ranking |
| Stream | Event logs, consumer groups | Simple pub/sub without persistence |
Pick the structure that matches how you read and write data — not how you would model it in SQL.
Best Practices
- Prefer hashes over JSON strings for objects with updatable fields
- Use key prefixes consistently:
app:entity:id:field - Set TTL on ephemeral data at creation time
- Keep individual values reasonably sized — split large documents across keys if needed
- Use
SCANfamily commands instead ofKEYSfor iteration
Common Mistakes
| Mistake | Impact |
|---|---|
| Storing everything as JSON strings | Cannot update fields atomically; higher memory |
| Using lists for unique membership checks | O(N) scans instead of O(1) set lookup |
| Huge hashes (millions of fields) | Slow HGETALL, memory pressure |
| ZSET with duplicate member names | Score updates overwrite — usually intended, but verify |
| Streams without MAXLEN trimming | Unbounded disk and memory growth |
Troubleshooting
Unexpected memory usage:
MEMORY USAGE user:1001
OBJECT ENCODING user:1001
DEBUG OBJECT user:1001 # dev/debug builds only
Slow commands on large collections:
SLOWLOG GET 10
# Replace SMEMBERS with SSCAN for large sets
SSCAN huge_set 0 COUNT 100
Performance Tips
HGETALLon large hashes is expensive — useHMGETfor specific fieldsZREVRANGEwith small ranges is O(log N + M) — efficient for top-N leaderboards- Pre-size sets with known cardinality when bulk loading: pipeline
SADDcommands - Use pipelining when inserting many keys of the same structure
Production Scenario
An analytics platform migrated user preference storage from JSON strings to hashes. Field-level updates (HSET user:123 theme dark) eliminated read-modify-write races and reduced average key size by 40%. Leaderboards moved from application-sorted lists to sorted sets, cutting update latency from 15ms to 0.3ms for 2M active players.
Understanding Redis data structures is the foundation for every pattern in this track — choose the right type before optimizing commands.