Featured image of post Caching & Redis: The 'Sticky Note' Mental Model

Caching & Redis: The 'Sticky Note' Mental Model

Why does Redis make everything faster? A mastery guide to cache invalidation (the hardest problem in CS), eviction strategies, and Redis data types.

“We’ll add Redis and it’ll fix the performance.”

Every engineer has said this. Few understand why it works — or when it catastrophically fails.

Caching is the single most powerful performance optimization you can apply. It is also the easiest way to serve stale data and have users see incorrect information for hours.

This is the Mastery Guide to Caching. We’ll cover Redis data types, the 3 eviction strategies, the hardest problem in Computer Science, and the patterns that separate seniors from juniors.


Part 1: Foundations (The Mental Model)

The Sticky Note vs. The Filing Cabinet

Every time your application needs data, it faces a choice:

  • The Database = The Filing Cabinet (in the basement) Accurate. Has everything. But you have to physically walk downstairs, open the drawer, find the folder, and walk back up. Slow: 5–100ms per trip.

  • The Cache (Redis) = The Sticky Note on your Monitor You already wrote the answer here earlier. Just look up. Fast: 0.1–1ms.

The goal: answer from the Sticky Note whenever possible. Only go to the Basement when the Sticky Note doesn’t exist or is outdated.

The Two Fundamental Questions

Every caching decision comes down to two questions:

  1. How long is the data “fresh”? (TTL — Time To Live)
  2. What happens when the cache is full? (Eviction Strategy)

Part 2: The Investigation (Redis Data Types)

Redis is not just a key-value store. It has 5 primary data structures, each solving a different problem.

TypeReal-World ObjectUse Case
StringA Sticky NoteSession tokens, simple cache values, counters.
HashA Mini-SpreadsheetUser profile (user:123 → {name, email, age}).
ListA Queue (FIFO)Activity feed (latest 50 actions). Task queues.
SetA Box of Unique MarblesUnique visitors today. Tags on a post.
Sorted SetA Ranked LeaderboardTop 10 users by score. Rate limiting windows.

The TTL (Time To Live)

Every cache entry should have a TTL — an expiration time. Without it, your cache is a memory leak.

1
2
3
4
5
6
7
8
9
import redis

r = redis.Redis()

# Set with a 5-minute TTL
r.set("user:123:profile", json.dumps(user_data), ex=300)

# Check remaining TTL
r.ttl("user:123:profile")  # Returns seconds remaining, -1 if no TTL, -2 if key gone

Part 3: The Diagnosis (Cache Invalidation — The Hardest Problem)

Phil Karlton famously said:

“There are only two hard things in Computer Science: cache invalidation and naming things.”

The 3 Eviction Strategies (When the Cache is Full)

When Redis runs out of memory, it must delete something. Which?

StrategyWhat Gets DeletedUse When
LRU (Least Recently Used)The key that hasn’t been accessed the longest.General purpose. Safe default. (“Delete the oldest stale data”).
LFU (Least Frequently Used)The key accessed the fewest times overall.When access frequency matters. (“Viral content stays; niche stays too”).
FIFO (allkeys-random)A random key.Almost never. Dangerous.

Set your eviction policy:

1
maxmemory-policy allkeys-lru

The 3 Cache Failure Patterns

1. Cache Miss (Normal) Key not in cache → fetch from DB → store in cache → return. This is expected.

2. Cache Stampede (Danger) TTL expires. 10,000 users all request the same data at the same moment. All of them miss. All of them hit the DB at once. The DB collapses.

Fix: Use a lock (Redis SETNX) when regenerating a cache entry. Only one process regenerates; others wait.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import redis, time

r = redis.Redis()

def get_heavy_data(key: str):
    cached = r.get(key)
    if cached:
        return json.loads(cached)

    # Only ONE process should regenerate
    lock_key = f"lock:{key}"
    if r.set(lock_key, "1", nx=True, ex=10):  # nx=SETNX (only if NOT exists)
        data = expensive_db_query()
        r.set(key, json.dumps(data), ex=300)
        r.delete(lock_key)
        return data
    else:
        time.sleep(0.1)
        return get_heavy_data(key)  # Retry

3. Cache Poisoning (Disaster) You cache wrong data. Now every user sees the wrong data until the TTL expires. There’s no easy fix — you must forcibly delete the key.

1
2
3
4
5
# Nuclear option: delete a specific key
redis-cli DEL "user:123:profile"

# Delete all keys matching a pattern (careful in production!)
redis-cli --scan --pattern "user:*" | xargs redis-cli DEL

Part 4: The Resolution (Python Cookbook)

1. The Cache-Aside Pattern (The Standard)

The application controls the cache. The most common pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def get_user(user_id: int) -> dict:
    cache_key = f"user:{user_id}"
    
    # 1. Try cache first
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)  # Cache HIT
    
    # 2. Cache miss: query the DB
    user = User.objects.get(id=user_id)
    data = serialize(user)
    
    # 3. Store in cache for next time (TTL: 5 minutes)
    redis_client.set(cache_key, json.dumps(data), ex=300)
    
    return data  # Cache MISS

2. Invalidate on Write (Keep the Cache Honest)

When data changes, delete the old cache entry immediately.

1
2
3
4
5
6
def update_user(user_id: int, new_data: dict):
    # 1. Update the DB
    User.objects.filter(id=user_id).update(**new_data)
    
    # 2. IMMEDIATELY invalidate the cache (don't wait for TTL!)
    redis_client.delete(f"user:{user_id}")

3. Rate Limiting (Redis as Counter)

A classic use case: limit an API to 100 requests per user per minute.

1
2
3
4
5
6
7
8
def is_rate_limited(user_id: int, limit: int = 100) -> bool:
    key = f"rate:{user_id}:{int(time.time() // 60)}"  # Key changes every minute
    
    count = redis_client.incr(key)  # Atomic increment
    if count == 1:
        redis_client.expire(key, 60)  # Set TTL on first increment
    
    return count > limit

Final Mental Model

1
2
3
4
5
6
Cache (Redis) -> The Sticky Note. Fast, temporary, might be outdated.
Database      -> The Filing Cabinet. Slow, accurate, permanent.

LRU Eviction  -> "Delete the one nobody visited in the longest time."
Cache Stampede -> 10,000 people flip over the same sticky note at once.
Cache Invalidation -> The hardest problem: knowing WHEN to throw away the sticky note.

Rules:

  • Set a TTL on everything. No TTL = Memory leak.
  • Invalidate cache on writes, not just by TTL.
  • Cache at the right layer: Avoid caching partial data that is hard to invalidate.
Made with laziness love 🦥

Subscribe to My Newsletter