3 min read
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.