One-liner: Caching stores the result of expensive operations (DB queries, API calls, computations) so future requests can be served instantly from fast memory instead of recomputing.
π Why Cache?
Without cache:
GET /user/42 β DB query (50ms) β response
With cache:
GET /user/42 β Redis lookup (1ms) β response β 50x faster!
Rule of thumb: RAM is ~100x faster than SSD, ~1000x faster than network DB calls.
πΊοΈ Caching Layers (From Client to DB)
[Browser Cache] β HTTP Cache-Control headers
β miss
[CDN Edge Cache] β Cloudflare, CloudFront
β miss
[Server-side Cache] β Redis, Memcached (in-memory)
β miss
[Database Query Cache] β MySQL query cache, Postgres
β miss
[Database Storage] β Actual data on disk
π Cache Reading Strategies
Cache-Aside (Lazy Loading) β Most Common
Read:
1. Check cache β HIT β return data
2. If MISS β query DB β store in cache β return data
Write:
1. Write to DB
2. Invalidate (delete) cache entry
def get_user(user_id):
cached = redis.get(f"user:{user_id}")
if cached:
return json.loads(cached) # cache hit β
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
redis.setex(f"user:{user_id}", 3600, json.dumps(user)) # cache for 1hr
return user
β
Only caches data that's actually requested
β
Cache failure is tolerable β fall back to DB
β Cache miss penalty: 2 trips (cache + DB)
β Stale data possible after writes (until TTL or invalidation)
Read-Through
App β [Cache Layer] β [DB]
(cache handles the DB lookup on miss)
The cache itself fetches from DB on miss. App only talks to cache.
β
Simplified app code
β First request is always slow (cold start)
Write-Through
Write β Cache β DB (synchronously)
On every write, update both cache and DB synchronously.
β
Cache always fresh
β Higher write latency (must write both)
β Writes cache entries that may never be read
Write-Behind (Write-Back)
Write β Cache β ACK to client
β (async, later)
DB
Write to cache first, asynchronously flush to DB.
β
Very fast writes
β Risk of data loss if cache crashes before flush
Use case: Analytics counters, view counts, likes
Write-Around
Write β DB (skip cache)
Read β Cache β if MISS β DB β store in cache
Writes go directly to DB, bypassing cache. Cache is populated on read.
β
Good for write-once, read-many data
β First read after write will miss cache
β οΈ Cache Eviction Policies
When cache is full, which items to remove?
| Policy | How It Works | Best For |
|---|---|---|
| LRU (Least Recently Used) | Remove item not accessed for longest | General purpose (Redis default) |
| LFU (Least Frequently Used) | Remove least accessed item | When access frequency matters |
| FIFO | Remove oldest inserted item | Simple use cases |
| Random | Remove random item | When access pattern is uniform |
| TTL | Remove items past their expiry | Time-sensitive data |
π TTL (Time to Live)
Every cache entry should have a TTL β or you risk stale data forever.
| Data Type | Suggested TTL |
|---|---|
| User session | 30 minutes (sliding) |
| User profile | 1β5 minutes |
| Product details | 5β30 minutes |
| Search results | 1β5 minutes |
| Homepage content | 1β10 minutes |
| Static config/flags | 1β24 hours |
| Rarely changing data | Days |
π₯ Cache Problems
Cache Stampede (Thundering Herd)
Popular cache key expires at exactly same time
β 10,000 concurrent requests all miss cache
β All hit the DB simultaneously
β DB crashes
Fix: Probabilistic Early Expiration β randomly refresh before TTL expires:
remaining_ttl = redis.ttl(key)
if remaining_ttl < 30 and random() < 0.1:
refresh_cache(key) # Only 10% of requests refresh early
Or: Mutex/Lock β only one request fetches from DB on miss.
Cache Penetration
Requests for non-existent keys (e.g., userId=-999)
β Always miss cache
β Always hit DB
β DB hammered with useless queries
Fix: Cache the null result too! redis.setex("user:-999", 60, "NULL")
Also: Bloom Filter β check if key exists before hitting DB.
Cache Avalanche
Many cache entries expire at the same time
β Flood of DB queries simultaneously
Fix: Add jitter (random variation) to TTLs:
ttl = 3600 + random.randint(-300, 300) # 3600 Β± 5 minutes
redis.setex(key, ttl, value)
π Key Takeaways
- Default to Cache-Aside for reads β it's the most flexible
- Always set a TTL β never cache indefinitely
- Handle thundering herd, penetration, and avalanche in production
- Cache only data that changes slowly and is read frequently
Top comments (1)
Great rundown β the write-strategy taxonomy is the part people memorize, but the part that actually decides correctness (and where interviews push) is invalidation. Cache-aside has a subtle race worth calling out:
You're serving stale data until TTL, with no write left to fix it. Two habits that help: on writes, delete the key rather than update it (an update races the same way), and on hot read paths reach for a delayed double-delete or a short lock around repopulation. It's also why write-through so often gets paired with cache-aside reads β the write path keeps the entry authoritative.
Small add to your stampede section: single-flight (one loader per key, everyone else awaits the same result) is usually easier to get right than probabilistic early expiration, and it composes cleanly with the TTL jitter you already use for avalanches.