Use-case gallery · 15 patterns · Picture first

Redis use cases, one picture each

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 path what breaks the key idea waiting or slow your 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 The app asks Redis first. A hit returns in about a fifth of a millisecond. A miss reads the database, then writes the value back to Redis with a five-minute TTL. ④ SET product:42 {…} EX 300 App your API Redis ~0.2 ms Database ~5–50 ms ① GET product:42 ② hit → done ③ miss → SELECT … FROM products Only misses reach the database. A 95% hit rate cuts DB reads 20×.
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.

Shared session store A browser holds only a cookie with a session id. Any of three stateless web servers can read the session hash from Redis. The TTL resets on every request. Browser sid=abc web-1 web-2 web-3 stateless — any server works HGETALL HGETALL session:abc (hash) user_id → 42 role → admin cart → 3 TTL 30 min · reset on each hit
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 lock expires under a paused worker Worker A takes the lock with a 30 second TTL, then pauses. The lock expires. Worker B takes it with fencing token 34 and writes. Worker A wakes and writes with token 33. Storage rejects 33 because it already saw 34. Worker A Redis key Worker B Storage lock ✓ #33 paused (GC) — still thinks it holds the lock owner A · TTL 30 s expires owner B · token #34 SET NX → (nil) lock ✓ #34 write #34 ✓ write #33 ✗ stale, rejected time →
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.

Lost update versus INCR Left: two clients both read 10 and both write 11, so one view is lost. Right: two INCR commands run one after another and the count reaches 12. GET then SET A: GET views → 10 B: GET views → 10 A: SET views 11 B: SET views 11 = 11 · one view lost ✗ INCR A: INCR views → 11 B: INCR views → 12 read + add + write happen as one step on one thread = 12 · exact ✓
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 window and its boundary burst Two one-minute windows, each with a limit of 100. A client sends 100 requests at 12:00:59 and 100 more at 12:01:00. Each window counts only 100, so 200 requests pass in two seconds. 12:00:00 12:01:00 12:02:00 limit 100 / window 200 requests in 2 s — both windows say OK rl:ip:12:00 → 100 rl:ip:12:01 → 100 Fix: sliding window ZSET of request timestamps
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.”

    One counter hands out IDs Three servers call INCR on order:id and get 1001, 1002, and 1003. A server can instead reserve a block of 1000 with INCRBY and hand those out locally. web-1 web-2 web-3 INCR order:id 1003 web-1 → #1001 web-2 → #1002 web-3 → #1003 Faster: INCRBY order:id 1000 reserves a whole block per server
    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?”

    A cart as a hash The key cart:user:42 holds fields for each product id with a quantity. Adding one item is HINCRBY, removing is HDEL, and the badge count is HLEN. cart:user:42 (hash) field qty sku:1001 2 sku:2040 1 sku:7788 3 +1 → HINCRBY … sku:1001 1 remove → HDEL … sku:2040 badge 🛒3 → HLEN cart:user:42
    One key per cart, one field per product. Each button on the page is one command on one field.
    Commands
    HINCRBY cart:user:42 sku:1001 1
    HDEL    cart:user:42 sku:2040
    HGETALL cart:user:42
    EXPIRE  cart:user:42 2592000   # 30 days
    In real life

    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.”

    Reliable queue with a processing list A producer pushes jobs onto the jobs list. A worker atomically moves one job into a processing list, works on it, and removes it when done. A janitor moves jobs stuck in processing back to the jobs list. Producer web request LPUSH jobs j3 j2 j4 BLMOVE processing j1 Worker sends email done → LREM (ack) janitor: stuck > 5 min (worker died) → move back
    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.

      👥 Mutual friends · dedupe

      Set
      Set intersection Two overlapping circles, followers of Ana and followers of Ben. SINTER returns the three people in both. ana ben 3 SINTER followers:ana followers:ben
      Commands
      SADD seen:webhooks evt_91  # 1 new, 0 dup
      SINTER followers:ana followers:ben
      Real life · Watch out

      “You both follow…”, tags, idempotent webhook handling. SMEMBERS on a million members blocks — use SSCAN.

      📍 Nearby drivers

      Geo (ZSet)
      Radius search A rider in the centre of a 2 km circle. Three drivers inside the circle are returned, two outside are not. 2 km GEOSEARCH … BYRADIUS 2 km ASC
      Commands
      GEOADD drivers 77.59 12.97 d17
      GEOSEARCH drivers FROMLONLAT 77.6 12.97
        BYRADIUS 2 km ASC COUNT 5
      Real life · Watch out

      Ride-hailing, food delivery, store finder. Radius and box only — no polygons, no road distance.

      👁 Unique visitors

      HyperLogLog
      HyperLogLog size One million visitor ids go in. The HyperLogLog stays at 12 kilobytes and reports about one million, within 0.81 percent. 1M ids tens of MB as Set HLL 12 KB max ≈ 1,000,000 ± 0.81% PFADD uv:home u42 · PFCOUNT uv:home
      Commands
      PFADD uv:2026-10-10 u42 u77
      PFCOUNT uv:2026-10-10
      PFMERGE uv:week uv:d1 … uv:d7
      Real life · Watch out

      Unique visitors, unique search terms, unique IPs. It only counts — you cannot list who visited.

      📣 Live push

      Pub/Sub
      Fan-out to listeners A publisher sends to a channel. Two connected subscribers receive it. A third, offline subscriber misses it forever. publish chat:7 ws-1 ✓ ws-2 ✓ offline ✗ fire-and-forget · no replay
      Commands
      SUBSCRIBE chat:7
      PUBLISH chat:7 '{"from":"ana","msg":"hi"}'
      Real life · Watch out

      Fan-out between WebSocket servers (Socket.IO Redis adapter), “clear your local cache” signals. A disconnected listener misses messages for good.

      📜 Event log with consumer groups

      Stream
      A stream read by a group An append-only log of six entries. A consumer group named billing splits new entries between two consumers, who acknowledge each one. 1 2 3 4 5 6 XADD acked pending group billing: c1 · c2 XREADGROUP · XACK · XAUTOCLAIM
      Commands
      XADD orders MAXLEN ~ 100000 * id 9 total 40
      XREADGROUP GROUP billing c1 COUNT 10
        STREAMS orders >
      XACK orders billing 1728550000000-0
      Real life · Watch out

      Order events, activity feeds, audit trails — Kafka-like without running Kafka. Always cap with MAXLEN or memory grows forever.

      Pick by problem

      I need to…

      Press a problem. The answer names the structure, the command, and the section.

      Cheat sheet
      StringCache · session · lock — SET … EX, SET … NX PX
      IntCounter · rate limit · ID — INCR, INCRBY, EXPIRE … NX
      HashCart · profile · session — HSET, HINCRBY
      BitmapDAU · retention · streaks — SETBIT, BITCOUNT, BITOP
      ListJob queue — LPUSH, BLMOVE, LREM
      ZSetLeaderboard · sliding window · delayed jobs — ZADD, ZRANGE … REV
      Set / Geo / HLLDedupe · nearby · uniques — SADD, GEOSEARCH, PFADD
      Pub/Sub / StreamLive push · event log — PUBLISH, XADD, XREADGROUP

      When Redis is the wrong tool

      Redis next to the database, not instead of it Good fits: hot, small, rebuildable, short-lived data. Bad fits: the only copy of money or orders, large blobs, ad-hoc queries and joins, and years of history. Good fit ✓ • hot: read many times per second • small: KB per key, not MB • rebuildable from the database • short-lived: TTL in minutes–days Wrong tool ✗ • the only copy of orders or money • files, images, big JSON blobs • ad-hoc queries, joins, reports • years of history (use a DB / Kafka)
      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.