Laravel Cache Tips: Speed Up Your App the Right Way
Caching is the highest-leverage performance change you can make in most Laravel apps. It is also one of the easiest to get subtly wrong in ways that cause stale data, thundering herds, and impossible-to-reproduce bugs. Here are the tips that keep caching fast and correct.
Use remember() Instead of Manual Checks
The most common caching pattern — check the cache, compute if missing, store, return — is a built-in one-liner. Stop writing it by hand:
// The verbose way nobody should write anymore:
$users = Cache::get('active_users');
if ($users === null) {
$users = User::where('active', true)->get();
Cache::put('active_users', $users, 3600);
}
// The right way:
$users = Cache::remember('active_users', 3600, function () {
return User::where('active', true)->get();
});
For data that rarely changes, rememberForever() skips the expiration entirely and you invalidate it manually when the underlying data changes.
Prevent Cache Stampedes With flexible()
Here is the bug that takes down sites: a popular cached value expires, and suddenly a thousand simultaneous requests all miss the cache and all run the expensive query at once. That is a cache stampede. Laravel's flexible() method implements stale-while-revalidate to prevent it:
$stats = Cache::flexible('dashboard_stats', [300, 600], function () {
return $this->computeExpensiveStats();
});
For the first 300 seconds the value is fresh. Between 300 and 600 seconds it is considered stale: requests get the stale value immediately while a single background refresh recomputes it. After 600 seconds it is fully expired. Only one request pays the cost of recomputing, and nobody waits. This one method eliminates an entire class of production incidents.
Lock Critical Sections With Atomic Locks
When two requests might do the same expensive or dangerous thing at once — generating a report, charging a card, sending a one-time email — use an atomic lock so only one proceeds:
$lock = Cache::lock('generate-report:' . $report->id, 120);
if ($lock->get()) {
try {
$this->generateReport($report);
} finally {
$lock->release();
}
}
// If the lock was not acquired, another process is already handling it
This requires an atomic-capable driver like Redis, Memcached, or a database. It is the difference between "the report generated once" and "we generated it five times and emailed the customer five invoices."
Cache Keys Should Be Specific and Invalidatable
A cache key like users is a trap — you can never tell what is in it or when to clear it. Build keys that encode exactly what they hold, including anything that affects the result:
$key = "user:{$user->id}:posts:page:{$page}:sort:{$sort}";
$posts = Cache::remember($key, 600, fn () => $user->posts()
->orderBy($sort)
->paginate(15, ['*'], 'page', $page));
When the data changes, clear the model in a saved/deleted event so you never serve stale records:
protected static function booted(): void
{
static::saved(fn (Post $post) => Cache::forget("user:{$post->user_id}:posts"));
}
Do Not Cache What You Cannot Afford to Be Stale
The last tip is a judgment call, not a method. Caching trades freshness for speed. A product catalog can be a few minutes stale with no harm; an account balance or inventory count usually cannot. Before you cache something, ask what happens if a user sees a value that is sixty seconds old. If the answer is "nothing," cache it aggressively. If the answer is "they overdraw their account," do not — or use a very short TTL plus an atomic lock around the write path.
Get these right and caching becomes the quiet workhorse it should be: dramatic speedups, no stampedes, no stale-data surprises. Reach for remember() and flexible() first, lock your critical sections, key things precisely, and always know the cost of being wrong before you trade away freshness.