Redis data structures
Learn which Redis data structure to use for timelines, counters, leaderboards, and sessions, with the specific commands and trade-offs each one makes.
TL;DR
Redis exposes several in-memory data structures, each with different commands, complexity, memory behavior, and ordering semantics. Choose the structure from the access patternβstrings for scalar values and counters, lists for sequences, sets for uniqueness, hashes for fields, and sorted sets for scores and ranges.
30-Second Explanation
Redis routes a key to one logical value, but the value's type determines which operations are efficient. INCR, LPUSH, SADD, HSET, and ZADD express different workloads and are atomic within Redis command execution. The trade-offs are memory usage, command complexity, expiration and persistence behavior, and whether the data needs exact ordering or uniqueness.
5-Minute Explanation
Start with the query you need to perform, then select the narrowest structure that supports it. A String handles a scalar or serialized blob; a Hash groups fields; a List preserves sequence; a Set answers membership and uniqueness; and a Sorted Set maintains a score-ordered index. The sections below connect those operations to common system-design workloads and show where each structure's complexity or memory cost becomes a constraint.
Why Redis data structures matter
The caching.mdx article covers cache patterns (cache-aside, write-through) and eviction policies. What it does not cover is which Redis structure to use for a given problem, and the commands that structure exposes.
In a system design discussion, naming Redis is only the starting point. The important choice is which data structure and command set match the access pattern. Using a List where a Sorted Set is needed, or a String where a Hash would fit better, changes ordering, complexity, and memory behavior.
This article covers the five structures you need to know and the canonical use cases for each.
Strings
The simplest structure. A key maps to a single value: a byte string, integer, or float.
Commands:
SET key value [EX seconds]β store a value, optionally with a TTLGET keyβ retrieve the valueINCR key/INCRBY key deltaβ atomic increment (returns new value)SETNX key valueβ set only if the key does not exist (used for distributed locks)MGET key1 key2 ...β fetch up to N keys in one round-trip
When to use:
- Atomic counters.
INCR rate_limit:{user_id}is the standard rate limiter primitive. The increment and read are atomic: no two clients can increment simultaneously and both read the same old value. - Session tokens.
SET session:{token} {user_id_json} EX 3600stores a session with a 1-hour TTL. One key, one lookup, one eviction via TTL. - Feature flags.
SET feature:{name} 1/GET feature:{name}. Simple, instant toggles. - Caching serialized objects.
SET tweet:{tweet_id} {serialized_tweet} EX 86400. This is the tweet content cache from the Twitter design.
What it does not do well: Anything where you need partial updates. Updating one field in a cached user object requires deserializing, modifying, and re-serializing the entire value. Use a Hash for objects with many independently updated fields.
Lists
An ordered sequence of strings. Insertion is O(1) from either end. Access by index is O(N). Think of it as a doubly-linked list.
Commands:
RPUSH key value [value ...]β append to the right (tail)LPUSH key value [value ...]β prepend to the left (head)LRANGE key start stopβ return elements from index start to stop (0-indexed; -1 = last)LTRIM key start stopβ remove everything outside [start, stop]; destructiveLLEN keyβ number of elementsLREM key count valueβ remove the firstcountoccurrences of a value
When to use:
- Recent-N caches.
LPUSH recent_activity:{user_id} {event_id}thenLTRIM recent_activity:{user_id} 0 99keeps the last 100 events. Combine:LPUSH + LTRIMin a pipeline; the list self-maintains its size cap. - Simple queues.
RPUSH queue:{name} taskto enqueue;LPOP queue:{name}to dequeue. Simple and fast, though message queues (Kafka, SQS) are preferable for durable distributed queues. - Fan-out feed (simple version). Before adding scores, Twitter's early architecture used
LPUSH timeline:{user_id} tweet_id+LTRIMto maintain a 800-entry feed. This works but cannot do time-range lookups within the list without scanning all items.
What it does not do well: Finding an element by value in the middle of a long list (O(N)). Sorting or ranking items by a score. Use a Sorted Set if you need ordered access by a computed score rather than insertion order.
Sorted Sets
A set of unique members each with a floating-point score. Members are stored sorted by score. All operations maintain the sorted order.
Commands:
ZADD key score member [score member ...]β add or update members. If member exists, updates its score (useful withGTflag to only update on higher scores).ZREVRANGE key start stop [WITHSCORES]β return members from highest to lowest score, by rankZRANGEBYSCORE key min max [LIMIT offset count]β return members with score within [min, max]ZREVRANGEBYSCORE key max minβ reverse: highest score first within rangeZREMRANGEBYRANK key start stopβ remove members by rank (used to trim a sorted set to N entries)ZCARD keyβ count of membersZSCORE key memberβ get a member's scoreZRANK key member/ZREVRANK key memberβ get the zero-indexed rank of a member (forward or reverse)
Time complexity: Most operations are O(log N) due to the underlying skip list structure.
When to use:
- Home timeline cache.
ZADD home_timeline:{user_id} {unix_timestamp} {tweet_id}stores tweet_ids scored by creation time.ZREVRANGE home_timeline:{user_id} 0 19returns the 20 most recent tweet_ids in one command.ZREMRANGEBYRANK home_timeline:{user_id} 0 -801trims to the most recent 800, discarding older entries. This is the core data structure powering Twitter's pre-computed feed. - Leaderboards.
ZADD leaderboard:{game_id} {score} {player_id}withGTflag ensures scores only update upward.ZREVRANGE leaderboard:{game_id} 0 9 WITHSCORESreturns the top 10.ZREVRANK leaderboard:{game_id} {player_id}returns a player's current rank in O(log N). - Rate limiters with time windows. Store request timestamps as members with the timestamp as the score.
ZREMRANGEBYSCORE key 0 {window_start}evicts old requests;ZCARD keycounts requests in the current window. This is the sliding window rate limiter pattern. - Scheduled delayed jobs. Store job IDs scored by their intended execution timestamp. A worker polls
ZRANGEBYSCORE jobs 0 {now} LIMIT 0 10to find jobs ready to run.
What it does not do well: Member values must be unique within a sorted set. If two tweets are scored identically (same millisecond timestamp) and you use the timestamp as both score and member, only one survives. Always use the unique identifier (tweet_id) as the member, and the sort key (timestamp) as the score.
Hashes
A map of field-value pairs stored under a single key. Think of it as an object or row: one Redis key, many fields.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.