Why Instagram Needed Caching
When Instagram hit 25 million users, their PostgreSQL database was handling 10,000 queries per second for user profile lookups alone. Each profile query took 5-15ms. They added a Redis cache in front. The same lookups now took 0.1ms. Database load dropped by 90%. The pattern they used? Cache-aside - the most common and most practical caching strategy in production systems.
How Cache-Aside Works
Cache-Aside Flow (Lazy Loading):
Application Cache (Redis) Database (PostgreSQL)
| | |
1. |---GET user:42---->| |
2a. |<--FOUND (HIT)-----| | (0.1ms - fast!)
| return data | |
| | |
--- OR if cache miss: --- |
2b. |<--NOT FOUND (MISS)-| |
3. |----SELECT * FROM users WHERE id=42----->|
4. |<---user data----------------------------| (5-15ms)
5. |---SET user:42 EX 300-->| | (cache for 5 min)
6. | return data | |
Key: the APPLICATION manages both cache and DB.
The cache never talks to the database directly.Code Example
import redis
import json
cache = redis.Redis(host='localhost', port=6379)
TTL = 300 # 5 minutes
def get_user(user_id: int) -> dict:
# Step 1: Check cache
cache_key = f"user:{user_id}"
cached = cache.get(cache_key)
if cached:
# Cache HIT - return immediately (0.1ms)
return json.loads(cached)
# Cache MISS - query database (5-15ms)
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
if user:
# Populate cache for future requests
cache.setex(cache_key, TTL, json.dumps(user))
return user
def update_user(user_id: int, data: dict):
# Update database first (source of truth)
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
# Invalidate cache (delete, don't update)
cache.delete(f"user:{user_id}")
# Next read will miss cache and re-populate from DBWhy 'Lazy Loading' Is Smart
Data is only loaded into the cache when it is actually requested. You never pre-fill the cache with data nobody asked for. This means your cache naturally contains the hottest data - the data real users are actively requesting.
The tradeoff: the first request for any piece of data will always be a cache miss (a 'cold start'). After a server restart, the first few hundred requests are slow as the cache warms up.
Cache Invalidation: The Hard Part
Phil Karlton famously said: 'There are only two hard things in computer science: cache invalidation and naming things.' Here is why:
The Stale Data Problem:
Time 0: Cache has user:42 = {name: "Alice", city: "Seattle"}
Time 1: Alice updates her city to "Portland" in the database
Time 2: Another service reads user:42 from cache
--> Gets {city: "Seattle"} - STALE!
Time 300: TTL expires, cache entry deleted
Time 301: Next read misses cache, fetches from DB
--> Gets {city: "Portland"} - finally correct
Invalidation Strategies:
Strategy 1: TTL-based (simplest)
- Set TTL = 300s (5 min max staleness)
- Tradeoff: shorter TTL = fresher data but more DB load
Strategy 2: Delete on write (most common)
- When writing to DB, delete cache key immediately
- Next read will miss and re-populate from DB
- Maximum staleness: milliseconds
Strategy 3: Event-driven invalidation
- Database publishes change events (CDC)
- Cache service subscribes and invalidates
- Works across multiple servicesWhen to Use Cache-Aside
Workload Pattern Cache-Aside? Why
------------------------ ------------ ---------------------------
Read-heavy (90%+ reads) YES Most reads served from cache
User profiles YES Read often, change rarely
Product catalog YES Millions of reads, few updates
Session data MAYBE Consider write-through instead
Real-time leaderboard NO Use Redis sorted sets directly
Financial balances NO Staleness is unacceptableNumbers That Matter
Redis GET latency: ~0.1ms (vs 5-50ms database query)
Cache hit ratio target: 95%+ for most applications
At 95% hit ratio with 10K req/sec: 9,500 cache hits + 500 DB queries instead of 10,000 DB queries
RAM cost: $5/GB/month on cloud vs CPU cost of database queries - usually a massive net savings
TTL sweet spot: 60-300 seconds for user-facing data, 3600+ seconds for rarely changing data
Interview Tip
When an interviewer asks about caching, start with cache-aside and explain why: 'The application checks Redis first. On a miss, it queries the database, stores the result in Redis with a 5-minute TTL, and returns it. On writes, I delete the cache key so the next read gets fresh data. This gives us sub-millisecond reads for 95%+ of requests while keeping the database as the source of truth.' Then mention the cold-start problem and how you would handle cache stampedes (locking or pre-warming).
Key Takeaway
Cache-aside is simple and effective: check cache first, fall back to database on miss, populate cache for next time. Invalidate on write by deleting the cache key. It works best for read-heavy workloads where slight staleness is acceptable. Most applications should start here.
