Caching is one of the easiest ways to speed up a Laravel application, and one of the easiest ways to create bugs that only show up some of the time. A customer sees an old price, an admin changes a setting and it "doesn't apply", or the cache empties and the database falls over under dozens of identical queries at once.

This short guide covers what to cache in Laravel 13 and what not to, how long to keep it, and how to make sure stale data never reaches your users.

Short answer: In Laravel, cache data that is expensive to build and rarely changes, such as statistics, menus and responses from external APIs, and always read financial figures and stock levels from the source of truth. Set the TTL by how much staleness is acceptable, from a few seconds to a day. For high-traffic pages use Cache::flexible, and for reliable invalidation put a version number or updated_at in the cache key.

What is worth caching

Before writing your first Cache::remember, answer one question: how expensive is this data to produce, and how quickly does it change? Caching pays off most when data is expensive and changes rarely.

Good candidates:

  • Results of heavy or aggregate queries, such as dashboard statistics, product counts per category or the week's best sellers.
  • Responses from external services, such as exchange rates or data from a slow API.
  • Data repeated on every page, such as the site menu, global settings and the category list.
  • Output of expensive processing, such as rendering Markdown to HTML.

Things that are better left uncached, or cached only with great care:

  • Stock levels, account balances and any number a financial decision is based on. Always read these from the source.
  • Personal user data under a key that doesn't include the user ID. One wrong key means showing one person's data to another.
  • Queries that are already fast. Caching a primary-key find() usually adds nothing but complexity.

Rule of thumb: measure first (with Telescope, Debugbar or the query log), then cache.

remember, rememberForever and flexible

remember

The most common pattern: return the value if it's cached; otherwise build it, store it and return it.

app/Http/Controllers/HomeController.php
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Support\Facades\Cache;

class HomeController extends Controller
{
    public function __invoke()
    {
        $bestSellers = Cache::remember('home:best-sellers', now()->addMinutes(10), function () {
            return Product::query()
                ->orderByDesc('sold_count')
                ->limit(8)
                ->get(['id', 'name', 'slug', 'price'])
                ->toArray();
        });

        return view('home', ['bestSellers' => $bestSellers]);
    }
}

One subtlety: remember treats null as "not in the cache". If your closure returns null, it runs again on every request. To represent "nothing found", return an empty array or false instead of null.

rememberForever

rememberForever stores the value with no expiry. It only suits data whose invalidation path you know precisely, for example settings that change only from the admin panel, which clears their cache at the same moment. If you're not sure, a long TTL (say, a day) is safer than "forever": at least mistakes fix themselves after a while.

flexible: slightly stale data, always-fast responses

Laravel's Cache::flexible implements the stale-while-revalidate pattern. Instead of one TTL, you pass two numbers:

php
$stats = Cache::flexible('dashboard:stats', [300, 900], function () {
    return [
        'orders_today' => Order::whereDate('created_at', today())->count(),
        'revenue_today' => (int) Order::whereDate('created_at', today())->sum('total'),
    ];
});

What [300, 900] means:

Age of the cached value Behaviour
Under 300 seconds Fresh; returned directly
Between 300 and 900 seconds The stale value is returned and refreshed after the response is sent
Over 900 seconds Expired; rebuilt immediately

The refresh runs with defer after the response has gone to the user, and sits behind a lock, so only one process rebuilds the value. For dashboards and busy pages where a few minutes of delay doesn't matter, flexible is usually the best choice.

How to choose a TTL

Set the TTL by asking: "What happens if a user sees this data X minutes out of date?"

  • Seconds to a minute or two: data that changes often but is read heavily, such as counters and the latest-posts list on a busy site. Even a 30-second cache on a page read several times per second cuts database load dramatically.
  • A few minutes to an hour: statistics, reports and responses from external APIs.
  • A few hours to a day: near-static data such as categories and menus, provided you've also implemented cache clearing on change.

Always write the TTL explicitly (now()->addMinutes(10) or a number of seconds) and avoid magic numbers scattered through the code. If the same TTL appears in several places, make it a class constant or a config value.

Cache keys: half the job

Most caching bugs come from bad keys. A few rules:

  1. Use a meaningful prefix: product:42:card beats p42.
  2. Include everything that changes the output: locale, user ID, page number, filters.
  3. Include a version or modification time so old entries become irrelevant on their own.

Putting updated_at in the key is the simplest approach. When the model is saved, updated_at changes and a new key is generated; the old key expires with its TTL.

app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class Product extends Model
{
    public function cachedDescriptionHtml(): string
    {
        $key = "product:{$this->id}:description:{$this->updated_at?->timestamp}";

        return Cache::remember($key, now()->addDay(), function () {
            return (string) Str::markdown($this->description ?? '');
        });
    }
}

For lists, keep a single "version" for the whole collection and put it in the key. Each change bumps the version and every previous key is effectively invalidated, without having to find and delete them one by one:

php
$version = Cache::rememberForever('products:version', fn () => 1);

$page = request()->integer('page', 1);

$products = Cache::remember("products:v{$version}:page:{$page}", 600, function () {
    return Product::query()->latest()->paginate(20)->toArray();
});

Clearing the cache with model events

When the TTL is long, you need to clear the cache when the data changes. Eloquent events are the natural place to do it:

app/Models/Category.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;

class Category extends Model
{
    protected static function booted(): void
    {
        $flush = function (): void {
            Cache::forget('categories:menu');

            // The category name also appears on product cards, so the product list must be invalidated too.
            // add only sets the value if the key doesn't exist; increment does nothing on a missing key.
            Cache::add('products:version', 1);
            Cache::increment('products:version');
        };

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

Two important caveats:

  • Model events only fire when you save through the model. Category::where(...)->update([...]) or a raw database query fires no events and won't clear the cache. Wherever you do bulk updates, clear the cache manually right there.
  • If the change happens inside a transaction, the cache may be cleared before the commit, and a concurrent request can put the old data straight back. In those cases, move the clearing until after commit with DB::afterCommit(...).

Tags only work on supporting drivers

Tags let you clear a group of keys at once:

php
Cache::tags(['products', 'category:5'])->remember('category:5:list', 600, fn () => /* ... */ []);

Cache::tags('category:5')->flush();

Not every driver supports tags, though. In Laravel 13, the redis, memcached, array and apc drivers do; database and file, the defaults in many projects, don't, and calling tags() on them throws an error. If you're on the database driver, use the versioned-key approach above, which works on every driver.

Preventing cache stampedes with a lock

Suppose the dashboard stats key expires and fifty requests arrive in that same second. All fifty find the cache empty and all fifty run the heavy query. This is called a cache stampede.

flexible largely solves this, because it rebuilds behind a lock. But if you need remember with a hard expiry, add the lock yourself:

app/Support/DashboardStats.php
<?php

namespace App\Support;

use App\Models\Order;
use Illuminate\Support\Facades\Cache;

class DashboardStats
{
    public static function get(): array
    {
        $key = 'dashboard:stats';

        if (($cached = Cache::get($key)) !== null) {
            return $cached;
        }

        // Only one process builds the value; the rest wait up to 10 seconds
        return Cache::lock("lock:{$key}", 30)->block(10, function () use ($key) {
            return Cache::remember($key, now()->addMinutes(5), fn () => [
                'orders' => Order::count(),
                'revenue' => (int) Order::sum('total'),
            ]);
        });
    }
}

Inside the lock we use remember again: requests that were waiting find the value in the cache when their turn comes and don't repeat the query. If the wait exceeds 10 seconds, block throws a LockTimeoutException, and you need to decide how to handle it. Locks work on the redis, database, file, memcached and dynamodb drivers, but with multiple servers the cache must be shared (Redis, for example), not file.

Don't cache objects; cache arrays and plain values

Laravel 13's config/cache.php has an option called serializable_classes, set to false in new projects. That means no PHP classes are unserialized when reading from the cache. The reason is security: if APP_KEY or access to Redis leaks, an attacker can't achieve code execution by planting a crafted object in the cache1.

The practical consequence: if you cache an Eloquent model, a Collection or even a Carbon instance, you get back a __PHP_Incomplete_Class instead of the original object, and your code fails somewhere further down.

php
// Wrong: a Collection of models
Cache::remember('categories:menu', 3600, fn () => Category::orderBy('position')->get());

// Right: a plain array
Cache::remember('categories:menu', 3600, fn () => Category::orderBy('position')
    ->get(['id', 'name', 'slug'])
    ->toArray());

If you really need to cache a particular class, you can allow it explicitly:

config/cache.php
'serializable_classes' => [
    App\Data\ExchangeRate::class,
],

But in most cases arrays and plain values are faster, take less space in the cache, and don't break when classes change in the next deploy. Store dates as strings or timestamps too, and convert them back to Carbon when you use them.

Frequently asked questions

What is the difference between Cache::remember and Cache::flexible in Laravel?

remember has a single TTL, and once it expires the next request has to rebuild the value on the spot. flexible takes two numbers and, in the window between them, returns the stale value immediately while rebuilding after the response behind a lock, so users never wait for the cache to be built.

How do I clear the Laravel cache when data changes?

In the model's saved and deleted events, remove the relevant key with Cache::forget or bump the key version with Cache::increment. Remember that bulk updates through the query builder don't fire model events, so clear the cache manually there.

Do the Laravel database and file cache drivers support tags?

No. Tags only work on drivers such as redis and memcached, and calling Cache::tags() on database or file throws an error. On those drivers, use versioned keys, which work everywhere.

Why does a cached Eloquent model become __PHP_Incomplete_Class in Laravel 13?

In new Laravel 13 projects, serializable_classes in config/cache.php is set to false, so no class is unserialized when reading from the cache. Cache plain arrays (for example via toArray()) instead of models and Collections.

What is a cache stampede and how do I prevent it in Laravel?

A cache stampede happens when a heavily used key expires and dozens of concurrent requests all run the same expensive query at once. Using Cache::flexible, or wrapping the rebuild in Cache::lock, ensures only one process builds the value while the others wait for its result.

Wrap-up

  • Measure first; cache only expensive, rarely changing data, and never sensitive financial figures.
  • For most high-traffic pages, Cache::flexible gives the best balance between freshness and speed.
  • Choose the TTL by acceptable staleness, and put everything that changes the output into the key.
  • Use updated_at or a version number in the key to make invalidation simple and reliable.
  • Tags only on Redis and Memcached; on database and file, use versioned keys.
  • For expensive keys, use a lock to prevent stampedes.
  • Cache arrays and plain values, not objects.
  1. The full set of cache options is in the official Laravel documentation: https://laravel.com/docs/cache ↩