We are still cooking the magic in the way!
Cache Invalidation Strategies
Cache Invalidation Strategies
Cache invalidation is one of the hardest problems in computer science. This lesson covers proven strategies for keeping your cache fresh and consistent with your data sources.
TTL-Based Expiration
The simplest invalidation strategy is to set a Time-To-Live (TTL) on cached items:
// Set cache with 1 hour TTL
Cache::put('user:' . $userId, $userData, 3600);
// Or using Redis directly
Redis::setex('product:' . $productId, 3600, json_encode($product));
// Check TTL remaining
$ttl = Redis::ttl('user:' . $userId); // Returns seconds remainingEvent-Based Invalidation
Invalidate cache when the underlying data changes:
// In your model or controller
public function updateUser($userId, $data)
{
// Update database
$user = User::findOrFail($userId);
$user->update($data);
// Invalidate cache
Cache::forget('user:' . $userId);
Cache::tags(['users'])->flush(); // If using cache tags
return $user;
}Tag-Based Invalidation
Group related cache keys under tags for bulk invalidation:
// Store with tags
Cache::tags(['users', 'user:' . $userId])->put('profile', $data, 3600);
Cache::tags(['users', 'admins'])->put('admin:list', $admins, 7200);
// Invalidate all user-related caches
Cache::tags(['users'])->flush();
// Invalidate specific user
Cache::tags(['user:' . $userId])->flush();Version-Based Cache Busting
Append a version number to cache keys instead of deleting old data:
// Store current version in Redis
Redis::set('cache_version:users', 1);
// Generate cache key with version
$version = Redis::get('cache_version:users');
$cacheKey = "users:list:v{$version}";
$users = Cache::get($cacheKey);
// To invalidate, increment version
Redis::incr('cache_version:users'); // Now version is 2
// Old cache at v1 becomes stale automaticallyCache Stampede Prevention
When cache expires on high-traffic keys, multiple requests may simultaneously try to rebuild the cache (stampede). Solutions:
public function getUserData($userId)
{
$cacheKey = 'user:' . $userId;
$lockKey = $cacheKey . ':lock';
// Try to get from cache
$data = Cache::get($cacheKey);
if ($data) return $data;
// Acquire lock (5 second timeout)
$lock = Cache::lock($lockKey, 5);
try {
if ($lock->get()) {
// Double-check cache (another process may have filled it)
$data = Cache::get($cacheKey);
if ($data) return $data;
// Rebuild cache
$data = User::find($userId);
Cache::put($cacheKey, $data, 3600);
return $data;
} else {
// Couldn't get lock, wait briefly and retry
usleep(100000); // 100ms
return Cache::get($cacheKey) ?? User::find($userId);
}
} finally {
$lock->release();
}
}public function getWithProbabilisticRefresh($key, $ttl, $callback)
{
$data = Redis::get($key);
if ($data) {
// Get remaining TTL
$remaining = Redis::ttl($key);
// Calculate probability of early refresh
// As TTL approaches 0, probability increases
$probability = 1 - ($remaining / $ttl);
if (mt_rand() / mt_getrandmax() < $probability) {
// Probabilistically refresh cache in background
dispatch(new RefreshCacheJob($key, $callback));
}
return json_decode($data, true);
}
// Cache miss - rebuild
$data = $callback();
Redis::setex($key, $ttl, json_encode($data));
return $data;
}Stale-While-Revalidate Pattern
Serve stale cache while refreshing in the background:
public function getWithStaleWhileRevalidate($key, $freshTtl, $staleTtl, $callback)
{
$data = Redis::get($key);
$freshUntil = Redis::get($key . ':fresh_until');
if ($data) {
$now = time();
if ($freshUntil && $now < $freshUntil) {
// Data is still fresh
return json_decode($data, true);
}
// Data is stale but usable - trigger background refresh
if (Redis::set($key . ':refreshing', 1, 'EX', 60, 'NX')) {
// We got the refresh lock
dispatch(new RefreshCacheJob($key, $freshTtl, $staleTtl, $callback));
}
// Return stale data immediately
return json_decode($data, true);
}
// Cache miss - synchronous rebuild
$data = $callback();
Redis::setex($key, $staleTtl, json_encode($data));
Redis::setex($key . ':fresh_until', $staleTtl, time() + $freshTtl);
return $data;
}Cache Invalidation Decision Tree
1. TTL-Based Expiration ✓ Simple, predictable data ✓ Low consistency requirements ✓ Example: Analytics dashboards, feeds 2. Event-Based Invalidation ✓ High consistency requirements ✓ Clear data change events ✓ Example: User profiles, product details 3. Tag-Based Invalidation ✓ Related data groups ✓ Bulk invalidation needs ✓ Example: Category products, user permissions 4. Version-Based Busting ✓ Complex invalidation logic ✓ Gradual rollout needs ✓ Example: Configuration, feature flags 5. Stale-While-Revalidate ✓ Expensive data generation ✓ High traffic keys ✓ Acceptable stale data ✓ Example: Homepage data, trending items
- Implement a product cache with tag-based invalidation (tags: products, category:{id}, brand:{id})
- Add cache stampede prevention using locks
- Create a method to invalidate all products in a category when category is updated
- Implement stale-while-revalidate for a homepage feed with 5-minute fresh TTL and 1-hour stale TTL
- Add monitoring to track cache hit rates and stampede occurrences