Cache Invalidation Strategies
Cache Invalidation Strategies
"There are only two hard problems in computer science: cache invalidation and naming things." Stale cache entries are a common source of subtle bugs. Laravel gives you several mechanisms to keep your cache in sync with the data it represents.
Tag-Based Invalidation
Cache tags are the most targeted invalidation tool Laravel provides. When you store a cached value, tag it with one or more labels. When the underlying data changes, flush only those tags:
// Storing — tag the cached article list
Cache::tags(['articles'])->put('article_list', $articles, 3600);
// Invalidating — when any article is created, updated, or deleted
Cache::tags(['articles'])->flush();
For finer granularity, tag by entity ID:
// Store a specific article's rendered page
Cache::tags(['articles', 'article:42'])->put('article:42:page', $html, 3600);
// Invalidate only article 42 when it is edited
Cache::tags(['article:42'])->flush();
Event-Driven Invalidation
Tie cache invalidation to Laravel's event system. This keeps your invalidation logic decoupled from your controllers and actions:
// app/Listeners/InvalidateArticleCache.php
class InvalidateArticleCache
{
public function handle(ArticleUpdated $event): void
{
Cache::tags(['articles', "article:{$event->article->id}"])->flush();
}
}
// Register in AppServiceProvider or via the #[Listen] attribute
use App\Events\ArticleUpdated;
use App\Listeners\InvalidateArticleCache;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Event::listen(ArticleUpdated::class, InvalidateArticleCache::class);
}
}
Now, whenever you fire an ArticleUpdated event anywhere in your codebase, the relevant cache entries are flushed automatically. No need to remember to clear the cache in every controller method.
TTL-Based Invalidation
For data that does not need to be perfectly fresh, simply let the cache entry expire. Set a TTL that matches how often the data changes:
// Product catalogue — updated daily, so cache for 1 hour
Cache::remember('products', 3600, fn() => Product::all());
// Homepage stats — updated every few minutes
Cache::remember('homepage_stats', 300, fn() => computeStats());
TTL-based invalidation is the simplest strategy and works well when eventual consistency is acceptable.
Model-Level Cache Invalidation
You can attach invalidation logic directly to your Eloquent model using the saved, deleted, and updated model events:
class Article extends Model
{
protected static function booted(): void
{
static::saved(function (self $article) {
Cache::tags(['articles', "article:{$article->id}"])->flush();
});
static::deleted(function (self $article) {
Cache::tags(['articles', "article:{$article->id}"])->flush();
});
}
}
This ensures that any time an Article is saved or deleted — regardless of where in your code it happens — the cache stays in sync.
Cache-Busting for Assets
For frontend assets (CSS, JavaScript, images), cache-busting prevents browsers from serving stale files after a deployment. Laravel's asset() helper with Vite handles this automatically in development, and Vite's manifest file handles it in production:
<!-- Vite handles cache-busting via hashed filenames -->
@vite('resources/css/app.css')
@vite('resources/js/app.js')
Choosing the Right Strategy
| Strategy | Best for |
|---|---|
| Tag-based flushing | Data that changes unpredictably; you need immediate freshness |
| Event-driven | Decoupled invalidation across a large codebase |
| TTL expiry | Data where slight staleness is acceptable |
| Model events | Simple apps where cache logic lives close to the model |
| Asset cache-busting | Static files served after deployments |
Tips
- Do not flush the entire cache when only a subset of data changed. Tags exist precisely to avoid this.
- Test your invalidation logic. Write a test that creates or updates a record and asserts that the relevant cache key is no longer present.
- In production, make sure your cache driver supports tags (Redis or Memcached). The
fileanddatabasedrivers do not.