Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Laravel Caching Strategies That Actually Help (and the Ones That Bite)

About Post

There's a moment in every Laravel project when a page gets slow and someone says, "Just cache it."

It works. The page is fast. And a week later, a manager asks why the dashboard still shows yesterday's numbers, or, worse, a user sees a list that belongs to someone else.

Caching is the quickest way to make a slow app fast and the quickest way to make a correct app wrong. The difference is a few habits around keys, invalidation and choosing what to cache in the first place.

First, decide if it deserves a cache

Good cache candidates tick three boxes:

  • Expensive: a heavy aggregate query, an external API call, a report that crunches thousands of rows.
  • Read far more often than it changes: settings, menus, dashboard stats, lookup lists.
  • Allowed to be a little stale: nobody is harmed if the "contracts this month" count is a minute behind.

If a query is slow because it's missing an index, fix the index. A cache on top of a bad query is a painkiller, not a cure, and the query still runs every time the cache expires.

Strategy 1: remember, with keys that include everything

Cache::remember() is the workhorse: return the cached value, or run the closure, store the result and return it.

$summary = Cache::remember(
    "tenant:{$tenant->id}:contracts:summary:{$status}:{$locale}",
    now()->addMinutes(10),
    fn () => ContractSummary::build($tenant, $status, $locale)->toArray()
);

The key is where the dangerous bugs live. Every input that changes the result must be in the key: the user or tenant, filters, page number, locale, role. Cache a per-user summary under a key like contracts:summary and the first user to load it decides what everyone else sees. That's not a performance bug. It's a data leak.

A simple habit: build keys with a consistent pattern, entity:id:what:variant, so they're readable in Redis and easy to find when you need to clear them.

Strategy 2: invalidate on model events

Time-based expiry is the simplest invalidation: the data is at most ten minutes old. When that's not good enough, clear the cache when the data changes.

class Setting extends Model
{
    protected static function booted(): void
    {
        $forget = fn () => DB::afterCommit(fn () => Cache::forget('settings:all'));

        static::saved($forget);
        static::deleted($forget);
    }
}

Two gotchas hide in this small snippet:

  • Transactions. If you clear the cache inside a transaction, another request can refill it with the old data before the transaction commits. DB::afterCommit() waits until the data is really saved (and runs immediately if there's no transaction).
  • Mass updates skip events. Setting::where('group', 'mail')->update([...]) runs one SQL query and never fires saved. If you use query-builder updates, clear the cache yourself next to them.

Strategy 3: tags or versioned keys for groups

Sometimes one change should clear many keys: every page and filter variation of a tenant's contract list, for example. Cache tags handle that:

Cache::tags(["tenant:{$tenant->id}"])->remember($key, 600, $callback);

// later, when anything for this tenant changes:
Cache::tags(["tenant:{$tenant->id}"])->flush();

Tags only work with stores that support them, like Redis and Memcached. The file, database and dynamodb drivers don't.

If your store doesn't support tags, use a version number in the key instead. Store tenant:42:cache-version, include it in every key for that tenant, and increment it when data changes. Old keys simply stop being read and expire on their own.

Strategy 4: stale-while-revalidate for hot keys

Here's a classic production problem: a popular cache key expires, and fifty requests arrive in the same second. All fifty see an empty cache, and all fifty run the expensive query at once. That's a cache stampede.

Laravel's Cache::flexible() handles this nicely:

$stats = Cache::flexible('dashboard:stats', [300, 900], fn () => Stats::build());

For the first 300 seconds, the value is fresh and served directly. Between 300 and 900 seconds, users still get the stale value instantly, while Laravel recalculates it once in the background after the response is sent. Only after 900 seconds does a request have to wait. For dashboards, it's often the best of both worlds.

For one-off expensive work, Cache::lock() is the other tool: one process takes the lock and builds the value, the others wait or back off.

Strategy 5: warm the cache before anyone asks

If a report is expensive and predictable, don't make the first user of the morning pay for it. Build it on a schedule:

// routes/console.php
Schedule::call(fn () => Cache::put('reports:occupancy', Report::occupancy(), now()->addDay()))
    ->dailyAt('05:00');

Warming also matters after deploys. If your deploy clears the whole application cache, every hot key goes cold at the same moment, right when traffic hits the new release. Clear only what the deploy actually changed, or warm the important keys straight after.

What I avoid caching

  • Anything about money or permissions that must be exact right now. Balances, payment status, "can this user approve this?" Stale here means wrong.
  • Whole Eloquent models with relations. They're large when serialised, and when the class changes in a deploy, old cached copies can behave strangely. Cache arrays or IDs instead.
  • Cheap queries. A primary-key lookup is already fast. Caching it adds a network round trip to Redis and another thing to invalidate.
  • Everything, "just in case". Each cached value is a promise to keep it correct. Make fewer promises.

The rule: before you cache something, answer two questions. What exactly goes in the key? And what event makes it wrong? If you can't answer both, don't cache it yet.

Quick checklist

  • Fix the query first; cache what's still expensive.
  • Every input that changes the result goes in the key.
  • Invalidate after commit, and remember mass updates skip model events.
  • Tags on Redis or Memcached; versioned keys elsewhere.
  • Cache::flexible() or locks for hot keys.
  • Warm predictable, expensive values on a schedule.

What's the strangest caching bug you've chased? Mine always seem to involve a key that was missing just one variable.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close