Every pattern below is one data structure plus two or three commands. Press ▶ Play on any diagram and it walks you through the request, step by step — including the step where it breaks.
Redis / the fast pathwhat breaksthe key ideawaiting or slowyour app or DB
The map: structure → use case
Pick the row by the question you ask. Click a chip to jump.
String: cache, session, lock
A string holds one value under one key. Add a TTL and it forgets on its own. Add NX and only one writer wins.
⚡ Cache
String
Question: “Have I already computed this?”
Cache-aside: read Redis, fall back to the DB, write the answer back with a TTL.
Commands
GET product:42 # → (nil) = miss
SET product:42 '{…}' EX 300
# on update in the DB:
DEL product:42
In real life
Product pages, API responses, rendered HTML, expensive SQL joins. Stack Overflow runs Redis as a shared cache behind each web server’s local cache.
Watch out
Stampede: a hot key expires, 1,000 requests miss at once. Let one rebuild with SET lock:product:42 1 NX EX 5, and add jitter to TTLs.
🍪 Session store
String → Hash
Question: “Who is this cookie?” — from any server.
The browser keeps an id. Redis keeps the state. Servers keep nothing, so you can add or kill them freely.
Commands
HSET session:abc user_id 42 role admin
EXPIRE session:abc 1800
# every request (sliding expiry):
HGETALL session:abc
EXPIRE session:abc 1800
In real life
Login state behind a load balancer. connect-redis for Express, Spring Session, Django and Rails session stores. Upgrade from a JSON string to a hash so “last seen” updates one field.
Watch out
allkeys-lru eviction or a restart without a replica logs everyone out. Use a replica, AOF, and noeviction or volatile-lru.
🔒 Distributed lock
String + NX + TTL
Question: “Only one worker may do this right now.”
A TTL keeps a dead worker from holding the lock forever, but it also lets a slow worker keep going after it lost the lock. A fencing token lets storage say no.
Lab · two workers, one lock
Worker Aidle
lock:invoicesfree
Worker Bidle
Press “A: acquire”, then “B: acquire”.
Commands
SET lock:invoices <uuid> NX PX 30000
INCR lock:invoices:fence # → 34
# release only your own lock (Lua):
if redis.call('GET',KEYS[1])==ARGV[1]
then return redis.call('DEL',KEYS[1]) end
return 0
In real life
A nightly billing job on 20 pods where exactly one must run. Redisson RLock (Java), redlock libraries for Node and Python.
Watch out
Never DEL blindly — you may delete someone else’s lock. A Redis lock is for efficiency. For money-grade correctness add fencing, or use etcd / ZooKeeper.
Int: counter, rate limit, IDs
A string that holds a number gets INCR. One thread runs it, so two clients can never read the same old value.
🔢 Counter
Int
Question: “How many?” — with thousands of writers.
Read-modify-write in the app races. INCR does it inside Redis, atomically.
Commands
INCR views:post:9 # → 1042
INCRBY likes:post:9 5
DECR stock:sku:1001 # < 0 → sold out
HINCRBY stats:2026-10-10 signups 1
In real life
View and like counts, flash-sale stock (“DECR, and if it went below 0, undo and say sold out”), live dashboards.
Watch out
Counts live in memory. Flush them to the DB every minute. Counting unique users? A counter double-counts — use HyperLogLog.
🚦 Rate limiter
Int + TTL · or ZSet
Question: “Has this caller done too much in the last minute?”
Fixed windows are one INCR and cheap. They let a burst through at the edge. A sorted set of timestamps counts the true last 60 seconds.
Lab · 5 requests per 10-second window
Window resets in 10 s
Send requests. The sixth one in the same window gets 429.
Commands
# fixed window
INCR rl:ip:1.2.3.4:202610101200
EXPIRE rl:ip:1.2.3.4:202610101200 60 NX
# sliding log (in one MULTI or Lua)
ZREMRANGEBYSCORE rl:u:7 0 (now-60000)
ZADD rl:u:7 now now
ZCARD rl:u:7
In real life
Public APIs (“5,000 requests per hour per token”), login and OTP attempts, per-tenant quotas. rate-limit-redis for Express, Rack::Attack for Rails.
Watch out
An in-process limiter counts per server — behind 10 servers the real limit is 10×. That is why the count lives in Redis. Make INCR + EXPIRE atomic or a crash leaves a key that never expires.
🆔 Global ID
Int
Question: “Give every server a unique next number.”
No two callers can get the same number, because one thread hands them out in order.
Commands
INCR order:id # → 1001
INCRBY order:id 1000 # → 3000
# this server now owns 2001–3000
In real life
Order and ticket numbers, short-URL codes (ID → base62 → bit.ly/3fZ), invoice sequences per tenant.
Watch out
Replication is async. After a failover the new primary may be behind and hand out an ID twice. Use AOF + WAIT, or Snowflake-style IDs. Public sequential IDs also leak your order volume.
Hash: shopping cart
A hash is a small object: many fields under one key. Change one field without rewriting the rest.
🛒 Shopping cart
Hash
Question: “What is in this cart, and how many of each?”
One key per cart, one field per product. Each button on the page is one command on one field.
Guest carts before login (key by session id), merged into the user cart on sign-in. The same shape fits user profiles, feature settings, and per-day stats.
Watch out
Store ids and quantities, not prices — read the price from the DB at checkout. The placed order goes to the database, not Redis.
Bitmap: user retention
A bitmap is a string you address bit by bit. User 42 is bit 42. 100 million users fit in 12.5 MB per day.
📅 User retention · daily actives
Bitmap
Question: “Of the users active on day 1, how many came back on day 7?”
Lab · click a cell to mark user active that day
Commands
SETBIT active:2026-10-01 42 1
BITCOUNT active:2026-10-01 # DAU
BITOP AND kept active:10-01 active:10-07
BITCOUNT kept # retained
BITOP OR week active:d1 … d7 # WAU
In real life
DAU / WAU / MAU, day-7 retention cohorts, login streaks (“7 days in a row” = AND of 7 bitmaps), per-user feature flags.
Watch out
Needs dense integer IDs. One user with id 4,000,000,000 allocates ~500 MB. UUIDs need an id map first. BITOP on huge keys blocks — run it on a replica.
List: message queue
A list keeps arrival order. Push on one end, pop from the other, and a worker waits for work without polling.
📬 Message queue (reliable)
List
Question: “Do this slow work later, once, in order.”
Plain BRPOP deletes the job the moment a worker takes it. If that worker crashes, the job is gone. Moving it to a processing list keeps a copy until the ack.
Lab · crash a worker and get the job back
jobs
processing
done (in DB)
Push two jobs, take one, then crash the worker.
Commands
LPUSH jobs '{"id":7,"to":"a@x.io"}'
BLMOVE jobs processing RIGHT LEFT 0
# after the work succeeds
LREM processing 1 '{"id":7,…}'
In real life
Emails, thumbnails, webhooks, PDF exports. GitHub built Resque on Redis lists; Sidekiq (Ruby) and BullMQ (Node) run on Redis today.
Watch out
No consumer groups, no replay. Need several consumer teams or history? Use Streams. Need days of retention at huge volume? Use Kafka.
ZSet: rank and leaderboard
A sorted set stores a score beside each member and keeps them ordered. Rank lookup is O(log N), even with millions of players.
🏆 Rank / leaderboard
ZSet
Question: “Who is top 10, and what is my rank?”
Lab · award points and watch the ranks move
Press a button. Redis re-sorts on every write; reading the top 10 costs nothing extra.
Commands
ZINCRBY board 10 ada
ZRANGE board 0 9 REV WITHSCORES # top 10
ZREVRANK board ada # 0 = 1st
ZUNIONSTORE week 7 board:d1 … board:d7
In real life
Game leaderboards, “top sellers today”, trending posts (score = votes with time decay), and delayed jobs (score = run-at timestamp).
Watch out
Equal scores sort by member name, not time. Want “first to reach 100 wins”? Encode time in the score. Use a key per day and union them for weekly boards instead of deleting old points.
Five more you will meet
The chart above stops at six structures. These five show up in almost every production Redis.
Redis is memory beside the database. If losing the last second of writes would hurt, the database owns that data.
Quick check
Your login rate limit works in dev but lets 10× the traffic through in prod. Why?Show answer
The counter lives in each server’s memory and prod has 10 servers. Move the counter to Redis: INCR rl:ip:… + EXPIRE … NX, so every server shares one count.
A worker took a job with BRPOP and crashed. Where is the job?Show answer
Gone. BRPOP removed it from the list. Use BLMOVE jobs processing so a copy stays until the worker acks, or use a Stream with a consumer group.
Worker A’s lock expired during a GC pause. Worker B now holds it. How do you stop A’s late write?Show answer
A fencing token. Each lock grant also does INCR lock:fence; the storage rejects any write whose token is lower than the last one it accepted.