Caching, performance and debugging
Cache stores and tags, remembering values correctly, route and config caching, hunting N+1 queries, Telescope, and Octane trade-offs.
Caching correctly
// remember: read-through with a TTL
$books = Cache::remember("author:{$author->id}:books", now()->addMinutes(30),
fn () => $author->books()->published()->get());
// tags need redis, memcached or dynamodb
Cache::tags(['books', "author:{$author->id}"])->flush();
// atomic lock for expensive recomputation
$lock = Cache::lock('report:daily', 60);
if ($lock->get()) {
try { Report::buildDaily(); } finally { $lock->release(); }
}
Cache::increment('book:'.$book->id.':views');
Cache::put('settings', $settings, now()->addDay());rememberForeveris a leak waiting to happen - always a TTL, even a long one.- The database cache store serialises into your primary database and adds contention; use it only when Redis is genuinely unavailable.
- Invalidate on the write path. A cache without eviction is a bug with a delay fuse.
⚠️
Cache the serialised DTO or array, never an Eloquent model with its loaded relations. A stale model in the cache bypasses casts, hides pending changes, and reappears with relations that no longer exist.
Finding the actual bottleneck
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan optimize # all of the above
php artisan optimize:clear # and clear them
php artisan telescope:install && php artisan migrate
php artisan db:monitor --databases=mysql --max=100| Symptom | Likely cause | Check |
|---|---|---|
| Slow list page | N+1 queries | Query count in Telescope or Debugbar |
| High memory in a worker | Loading a whole table | lazyById() instead of get() |
| Slow first request only | Uncached config and routes | optimize in the deploy script |
| High DB CPU with simple queries | Missing index | EXPLAIN the generated SQL |
| Slow responses, fast queries | Serialisation or view rendering | Profiler step timings |
// queue a slow side effect so the response does not wait
ProcessCover::dispatch($book);
// select only what the template needs
$books = Book::query()
->select(['id', 'title', 'author_id'])
->with('author:id,name')
->paginate(25);Octane and long-running workers
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:start --workers=auto --max-requests=500- Octane keeps the application in memory, so state that used to be per-request now leaks: static properties, singletons holding a user, and container bindings that captured a request.
- Reset what you cache: list your singletons and add
Octane::flush()or aRequestReceivedlistener that clears them. - Measure before adopting it. Octane removes framework bootstrap cost, which is a few milliseconds on a warm OPcache - the win is real, but smaller than a single unindexed query.
FAQ
When should I reach for cache tags?
When one write must invalidate several related entries and you cannot name them all - for example every listing that includes a given tag. Tags require a taggable driver, so the code must not rely on them if the cache store could change.
Is Redis always the answer?
For shared caching and locks in a multi-instance deployment, usually yes. For a single instance, an in-process array cache plus OPcache often removes the network hop entirely - measure your actual cache hit ratio first.
Related
Eloquent relationships, casts and query scopes Deployment and production hardening
Last refreshed 2026-09-18.