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:
- How long is the data “fresh”? (TTL — Time To Live)
- 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.
| Type | Real-World Object | Use Case |
|---|---|---|
| String | A Sticky Note | Session tokens, simple cache values, counters. |
| Hash | A Mini-Spreadsheet | User profile (user:123 → {name, email, age}). |
| List | A Queue (FIFO) | Activity feed (latest 50 actions). Task queues. |
| Set | A Box of Unique Marbles | Unique visitors today. Tags on a post. |
| Sorted Set | A Ranked Leaderboard | Top 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.
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?
| Strategy | What Gets Deleted | Use 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:
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.
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.
# 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.
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.
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.
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
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.
Related posts
-
CDN: The 'Local Convenience Store' Mental Model
Why does your image load instantly for users in the US but crawls in Vietnam? A mastery guide to CDN Edge Nodes, Cache-Control headers, and cache busting.
-
Task Queues & Message Brokers: Celery, RabbitMQ, and Kafka Untangled
Why does sending an email block your API? A mastery guide to async task queues (Celery/Django-Q), message brokers (RabbitMQ), and event streaming (Kafka).
-
Rate Limiting & Circuit Breaker: The 'Traffic Light & Fuse Box' Mental Model
How do you stop one bad client from taking down your entire API? A mastery guide to rate limiting strategies, circuit breakers, and resilience patterns.
-
Why Cheap Hardware Won't Make Redis Replace a Core Database
As hardware gets cheaper, why not use Redis as the main database? The short answer: because the limitation isn't speed. It's guarantees and data semantics.