Redis Cheat Sheet: Commands by Data Type, TTL, and Real-World Patterns
A practical Redis cheat sheet covering commands by data type, TTL and expiry, and patterns for caching, rate limiting, and queues, with copy-paste examples.
Redis Cheat Sheet: Commands by Data Type, TTL, and Real-World Patterns
Redis is fast because it keeps everything in memory and gives you the right data structure for each access pattern. The trick is knowing which structure to reach for. A counter is not a queue, a leaderboard is not a hash, and using the wrong one means writing client-side glue that Redis would have done in one command. This guide organizes the commands the way you actually think about them: by data type, then by the patterns you build on top.
For a searchable version of every command below, with a one-line pitfall on each entry, keep the Redis cheat sheet open in another tab while you read.
Strings: counters, flags, and cached blobs
Strings are the default key type, and they hold more than text. A string can be a JSON blob, a serialized object, or an integer you increment atomically.
SET session:abc "user-42" EX 3600 # value plus a 1-hour TTL in one command
GET session:abc
INCR page:views # atomic +1, returns the new value
INCRBY cart:total 250
DECR stock:sku-9
APPEND log:line "more text"
STRLEN session:abc
SETNX lock:job "owner-1" # set only if the key is absent
INCR matters because it is atomic. Two processes incrementing the same counter never lose an update, which is exactly what you want for view counts, rate buckets, or sequence IDs. A common mistake: a plain SET to refresh a value silently wipes any TTL the key had. Use SET key value KEEPTTL or re-specify EX so the expiry survives.
Hashes, lists, sets, and sorted sets
A hash is one object stored under one key, with named fields. Use it for a user record, a config bundle, or anything you would otherwise serialize to JSON.
HSET user:42 name "Mia" plan "pro" seats 5
HGET user:42 plan
HMGET user:42 name seats # batch read only the fields you need
HINCRBY user:42 seats 1
Avoid HGETALL on a hash you let grow to millions of fields, it returns megabytes in one blocking reply. Use HSCAN with a COUNT, or HMGET the specific fields.
A list is an ordered sequence, perfect for queues. Push on one end, pop from the other.
LPUSH queue:emails "msg-1" # push to the head
RPUSH queue:emails "msg-2" # push to the tail
RPOP queue:emails # pop the oldest (FIFO with LPUSH+RPOP)
LRANGE queue:emails 0 -1 # inspect everything
BLPOP queue:emails 5 # block up to 5s waiting for an item
BLPOP is the one to remember: a worker can block on an empty queue instead of polling in a busy loop.
A set is an unordered collection of unique members, ideal for membership tests and relationships.
SADD online:users 42 51 67
SISMEMBER online:users 42 # is this user online? 1 or 0
SUNION followers:a followers:b # combined audience
SINTER followers:a followers:b # mutual followers
A sorted set (zset) keeps members ordered by a numeric score. It is the structure behind leaderboards and time-based schedules.
ZADD leaderboard 980 "mia" 1200 "leo"
ZRANGE leaderboard 0 9 REV WITHSCORES # top 10, highest first
ZINCRBY leaderboard 50 "mia"
ZADD jobs 1718200000 "send-invoice" # score = unix timestamp
ZRANGEBYSCORE jobs -inf 1718203600 # everything due up to now
Storing a timestamp as the score turns a sorted set into a due-date queue: poll with ZRANGEBYSCORE -inf now, process the entries, then remove them with ZREM.
TTL and expiry
Every Redis key can carry an expiry, and getting this right is the difference between a cache and a memory leak.
SET cache:home "<html>" EX 60 # expire in 60 seconds
EXPIRE report:weekly 604800 # set a 7-day TTL on an existing key
TTL report:weekly # seconds left, -1 = no TTL, -2 = gone
PERSIST report:weekly # remove the TTL, make it permanent
SET draft:7 "..." PX 500 # 500 millisecond TTL
The pattern that bites people: refreshing a value with a bare SET resets the key but drops the TTL. If you want a 60-second cache entry to stay a 60-second entry across writes, always write the expiry alongside the value: SET cache:home "<html>" EX 60.
Common patterns: caching, rate limiting, queues
Caching is the headline use case. The cache-aside flow is three lines: try the cache, fall through to the source on a miss, write back with a TTL.
GET product:99 # miss returns (nil)
# ... fetch from PostgreSQL ...
SET product:99 "{...}" EX 300 # cache for 5 minutes
Rate limiting uses an atomic counter with an expiry. Increment a per-user, per-window key and set the TTL only on the first hit:
INCR rate:user-42:minute # returns 1 on the first request this minute
EXPIRE rate:user-42:minute 60 # window resets after 60s
# if the returned count exceeds your limit, reject the request
Queues ride on lists for fire-and-forget jobs (RPUSH to enqueue, BLPOP to consume) or on Streams when you need durability and consumer groups. Use Streams when losing a job is unacceptable; cap them with XADD stream MAXLEN ~ 1000000 so they do not grow forever.
A worked example: caching a value with a TTL
Here is a flow I run almost every day when I add a cache in front of a slow query. Say a dashboard endpoint joins three tables and takes 400ms. I cache the rendered JSON for five minutes.
First, check the cache:
GET dashboard:team-7
It returns (nil) on the first call. So I run the real query, render the response, and write it back with a TTL in a single command:
SET dashboard:team-7 "{\"revenue\":48210,\"users\":312}" EX 300
Now every request for the next five minutes hits memory. I confirm the expiry is actually set, because a SET without EX would have made this permanent and stale:
TTL dashboard:team-7
That returns something like 287, meaning 287 seconds remain. When the data updates, I do not wait for expiry, I invalidate immediately with DEL dashboard:team-7 so the next read repopulates a fresh value. The whole loop is GET, SET ... EX, TTL to verify, and DEL on writes. It took me longer to write this paragraph than to wire it up.
A quick safe-distributed-lock note
Reach for SET resource_lock token NX EX ttl. NX acquires only if the key is absent, EX guarantees the lock self-releases if the holder crashes, and token is a random per-acquirer string. Release with a small Lua script that compares the token before DEL, never a plain DEL, or two processes can step on each other after expiry. The classic bug is pairing SETNX with a separate EXPIRE: if the client crashes between the two commands, the lock never expires.
Quick reference
| Task | Command | | --- | --- | | Set with TTL | SET key val EX 60 | | Atomic counter | INCR key | | Object field | HSET key field val | | Enqueue / dequeue | LPUSH key v / RPOP key | | Blocking consume | BLPOP key 5 | | Membership test | SISMEMBER key member | | Leaderboard add | ZADD key score member | | Due-by-time scan | ZRANGEBYSCORE key -inf now | | Set / read TTL | EXPIRE key 60 / TTL key | | Scan safely | SCAN 0 MATCH user:* COUNT 1000 |
Never run KEYS * in production, it is O(N) and blocks the single thread for seconds on a large database. Use SCAN and dedupe client-side, since SCAN can return the same key twice across iterations.
Redis rarely lives alone. Once your cache is warm, the slow source behind it is usually a relational database, so it pairs naturally with the PostgreSQL cheat sheet when you are writing the cache-aside query. Bookmark both, and you have the read path covered end to end.
Made by Toolora · Updated 2026-06-13