Command Palette

Search for a command to run...

Unit 3.1 · Redis and Caching

Data Structures and Caching Patterns

Redis's core types and what each is for, TTLs, cache-aside vs write-through, invalidation, cache stampedes, and sessions and rate limiting in Redis.

Beginner 45 min 4 lab steps 2 failure drills

Start here

The mental model

Redis is a giant dictionary living in RAM, where the values aren't just strings but data structures: lists, sets, sorted sets, hashes. Reading RAM is ~1000× faster than reading a database's disk-backed row over a query planner, so we put copies of hot data in Redis to protect the database and speed up pages.

A cache is a bet that you can serve slightly stale data in exchange for speed. Every caching bug is one of three things: the data is stale for too long (invalidation), too many requests miss at once and flood the database (stampede), or the cache disappears and the database can't cope without it (dependency).

Go deeper

How it works inside

01The core types

STRING: bytes up to 512 MB, counters with INCR (atomic). HASH: a small object's fields (HSET user:42 name Asha plan pro). LIST: a linked list, used as a simple queue (LPUSH/BRPOP). SET: unique members (SADD online-users). SORTED SET: members ranked by score, for leaderboards, top-N, and time-ordered indexes (ZADD, ZRANGE ... REV). Plus streams (Unit 2.4), bitmaps, HyperLogLog (approximate unique counts in 12 KB), and geo indexes.

Redis executes commands on ONE main thread, one at a time, so every single command is atomic, and no locks are needed for INCR or ZINCRBY. The flip side: one slow command (KEYS * on 10 million keys, SMEMBERS on a huge set, a big Lua script) blocks every other client (Unit 3.2).

02Caching patterns

CACHE-ASIDE (lazy loading), the most common: the app reads Redis; on a miss, it reads the database and writes the result to Redis with a TTL. Simple, and the cache only holds what's used, but the first request after expiry is slow. WRITE-THROUGH: the app writes to the database and updates the cache in the same code path, so data is fresh but you cache things nobody reads. WRITE-BEHIND: write to the cache and flush to the database asynchronously. Fast, but a Redis failure loses writes; rarely worth it.

Invalidation: on update, DELETE the cache key rather than setting it (setting it risks racing with a concurrent reader that writes back an older value). Always set a TTL anyway, as a safety net for any invalidation you missed. Add small random JITTER to TTLs so keys written together don't all expire together.

Caching patternsdiagram
Rendering diagram…

03Cache stampede

When a very hot key expires, hundreds of concurrent requests miss at the same instant and all run the same expensive query: the THUNDERING HERD. Defences: a short lock so only one request rebuilds (SET lock:product:42 1 NX EX 5, and others briefly wait or serve stale data), early probabilistic refresh before expiry, 'stale-while-revalidate' (store a soft expiry inside the value and refresh in the background), and request coalescing in the app.

04Beyond caching: sessions and rate limits

Sessions in Redis let any app replica serve any user, which is what makes the web tier stateless (Kubernetes course, Deployments). Give them a TTL that matches the session lifetime. RATE LIMITING: a fixed window is INCR rl:user42:202609271014 with EXPIRE 60; a sliding window uses a sorted set of request timestamps or a small Lua script for atomicity. API gateways and NGINX (Networking course, NGINX features) often use Redis for exactly this.

Do it

Hands-on lab

  1. 1

    Start Redis and try each type

    terminal
    $ docker run -d --name redis -p 6379:6379 redis:8
    alias rc='docker exec -i redis redis-cli'
    rc SET page:views 0 && rc INCR page:views
    rc HSET user:42 name Asha plan pro && rc HGETALL user:42
    rc ZADD leaderboard 1200 asha 950 ravi 1500 mei && rc ZRANGE leaderboard 0 2 REV WITHSCORES
    ── expected output ──
    OK
    (integer) 1
    (integer) 2
    1) "name"
    2) "Asha"
    3) "plan"
    4) "pro"
    (integer) 3
    1) "mei"
    2) "1500"
    3) "asha"
    4) "1200"
    5) "ravi"
    6) "950"
  2. 2

    TTLs and expiry

    EX sets seconds; TTL shows what remains (-1 = no expiry, -2 = key gone).

    terminal
    $ rc SET product:42 '{"name":"Mug","price":399}' EX 5 && rc TTL product:42
    sleep 6; rc GET product:42; rc TTL product:42
    ── expected output ──
    OK
    (integer) 5
    (nil)
    (integer) -2
  3. 3

    Cache-aside with a stampede lock (Python)

    The rebuild lock means only one request queries the database when a hot key expires; the others wait briefly and re-check. The random jitter spreads expiry times.

    product_cache.pywhole filepython
    import json, random, time, redis
    r = redis.Redis()
    
    def get_product(pid: int):
        key = f"product:{pid}"
        for _ in range(50):                                   # wait up to ~1s for a rebuild
            if (cached := r.get(key)) is not None:
                return json.loads(cached)
            if r.set(f"lock:{key}", "1", nx=True, ex=5):      # I rebuild it
                try:
                    row = db_fetch_product(pid)               # the expensive query
                    r.set(key, json.dumps(row), ex=300 + random.randint(0, 60))
                    return row
                finally:
                    r.delete(f"lock:{key}")
            time.sleep(0.02)                                  # someone else is rebuilding
        return db_fetch_product(pid)                          # fallback: don't fail the request
    
    def update_product(pid: int, fields: dict):
        db_update_product(pid, fields)
        r.delete(f"product:{pid}")                            # invalidate, don't set
  4. 4

    A fixed-window rate limiter

    INCR returns the new count; EXPIRE ... NX starts the 60-second window only on the first request. The app allows at most 5 per window. In production, send both in one pipeline or a MULTI/EXEC so a crash between them can't leave a key without expiry.

    terminal
    $ for i in 1 2 3 4 5 6; do rc INCR rl:user42; rc EXPIRE rl:user42 60 NX > /dev/null; done
    ── expected output ──
    (integer) 1
    (integer) 2
    (integer) 3
    (integer) 4
    (integer) 5
    (integer) 6 <- over the limit: the app returns HTTP 429

Operate it

Knobs that matter

SettingDefaultWhat it doesWhen to change it
TTL per keynoneWhen the key expires.Always set one for cache entries; add jitter; shorter for data that changes often.
Client timeoutslibrary-specific (often none)How long the app waits for Redis.Set short (50–200 ms) connect and command timeouts so a sick cache fails fast instead of hanging requests.
Connection pool sizelibrary-specificConnections per app instance.Small is fine: Redis is single-threaded and pipelining is more effective than many connections.

3am practice

Failure drills

Each drill is a real failure mode. Read the scenario and the output, decide what's wrong, then reveal the diagnosis.

Drill #1

The database melts every hour on the hour

A popular homepage query is cached with EX 3600. Every hour, DB CPU spikes to 100% for about a minute and p99 latency jumps.

terminal
$ psql -c "select calls, mean_exec_time from pg_stat_statements where query like '%homepage_feed%'"
── what you'll see ──
calls | mean_exec_time
--------+----------------
38214 | 1841.6

Drill #2

Users see old prices after an update

Price changes appear on some product pages minutes later, randomly, even though the update code deletes the cache key.

terminal
$ grep 'product:42' app.log
── what you'll see ──
10:00:00.010 req-A cache miss product:42, reading DB (price 499)
10:00:00.015 admin updates price to 399, DEL product:42
10:00:00.090 req-A SET product:42 (price 499) EX 300

Decide

Caching patterns

PatternRead pathWrite pathGood forRisk
Cache-asideCache, then DB on missWrite DB, delete keyMost read-heavy dataStale on races, stampedes
Write-throughCacheWrite DB and cache togetherData read soon after writingCaching unread data
Write-behindCacheCache now, DB laterVery high write rates (counters)Losing writes if Redis dies
Read-throughCache loads from DB itself—Managed caches / librariesSame as cache-aside

The bigger picture

Connects to

Prove it

Interview questions

01

Explain cache-aside and its main pitfalls.

02

How do you prevent a cache stampede?

03

Why is Redis single-threaded and why is it still fast?

0/3 · 0%