Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
LaravelArchitecture guide

Laravel 13 caching strategy: flexible, memo, tags and locks

Design a Laravel 13 cache layer that stays fast and correct: choose a store, key and invalidate data, serve stale values safely, and stop cache stampedes.

Cloudsoft Zone

7 min read

Close-up of wooden library card catalogue drawers with brass label holders and pull handles
Photograph: MarkBuckawicki / CC0, via Wikimedia Commons
In this article

A good Laravel 13 cache strategy has four parts: a shared in-memory store such as Redis, predictable keys, a clear rule for when each value is invalidated, and protection against many requests rebuilding the same value at once. Laravel 13 gives you tools for all four, including Cache::flexible() for stale-while-revalidate, Cache::memo() for per-request memoization, Cache::touch() for extending TTLs, atomic locks and a failover store.

This article explains how those pieces fit together and when to use each one. It was written against the Laravel 13.x cache documentation, the 13.x upgrade guide and the framework source as of 10 October 2026 (Laravel 13.35.0).

Start with the store

New Laravel applications use the database cache driver by default. That is a sensible starting point, because it needs nothing beyond your database, but it puts cache traffic on the same server as your queries and does not support cache tags. For most production applications the choice looks like this:

StoreTagsAtomic locksGood fit
redisYesYesDefault choice for multi-server apps; also backs queues and sessions
memcachedYesYesPure caching at scale; "forever" items can be evicted when memory is full
databaseNoYesSmall apps and single servers with no Redis
fileNoYesOne server only; every server has its own copy
dynamodbNoYesServerless and AWS-native deployments
arrayYesYesTests; values live only for the current process

The tag column follows the documentation, which says tags are not supported on the file, dynamodb, database or storage drivers. Locks require memcached, redis, dynamodb, database, file or array, and every server must talk to the same central cache.

Keep the app running when the cache fails

Laravel 13 ships a failover driver. It tries each store in order and moves to the next one when an operation fails, then dispatches an Illuminate\Cache\Events\CacheFailedOver event you can log or alert on:

PHP
// config/cache.php, inside 'stores'
'failover' => [
    'driver' => 'failover',
    'stores' => [
        'redis',
        'database',
    ],
],

Then set CACHE_STORE=failover in .env. A Redis outage now slows the site down instead of taking it offline.

Design keys you can reason about

Cache bugs are usually key bugs: two different values sharing a key, or a key that never changes when the data does. A simple convention avoids most of them:

  • Prefix by domain and record: product:42:summary, user:7:permissions.
  • Include anything that changes the output: locale, currency, page number, filters, a schema version you bump when the cached shape changes (v2:product:42:summary).
  • Never put raw user input into a key without normalising it; hash long filter strings.

Laravel 13 changed the default key prefix to a hyphenated form based on APP_NAME (for example my-app-cache-). If several applications share one Redis database, set CACHE_PREFIX explicitly so they never collide, and remember that Cache::flush() ignores the prefix and empties the whole store.

Pick a read pattern per kind of data

remember: simple and strict

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

$summary = Cache::remember("product:{$id}:summary", now()->addMinutes(10), function () use ($id) {
    return Product::query()
        ->whereKey($id)
        ->withCount('reviews')
        ->firstOrFail()
        ->only(['id', 'name', 'price', 'reviews_count']);
});

Use remember when a value must never be older than its TTL. Note that the callback returns a plain array, not a model. That matters in Laravel 13, as explained below.

flexible: fast reads for data that can be slightly stale

When a remember entry expires, the next visitor waits while it is rebuilt. Cache::flexible() implements stale-while-revalidate: you pass two durations in seconds. Within the first, the value is fresh. Between the first and second, the stale value is returned immediately and Laravel registers a deferred function to refresh it after the response is sent. After the second, the value is rebuilt in the request.

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_cents'),
    ];
});

This suits dashboards, counters, listings and "popular this week" blocks. It does not suit prices at checkout, stock levels or permissions, where stale data causes wrong decisions.

memo: stop hitting Redis repeatedly in one request

Even a fast cache costs a network round trip. If the same key is read many times in one request or job, for example a settings array read by several components, wrap it in Cache::memo():

PHP
$settings = Cache::memo()->get('site:settings');

// Later in the same request: served from memory, no Redis call
$settings = Cache::memo()->get('site:settings');

Writes through the memoized repository (put, increment, remember and so on) forget the memoized value and go to the underlying store, so you do not read your own stale write.

Invalidation: decide before you cache

Every cached value needs one of three invalidation rules, chosen up front:

  1. Time only. The TTL is the contract. Good for aggregates and third-party API responses.
  2. Event-driven. Forget the key when the source record changes. Model events keep this in one place:
    PHP
    class Product extends Model
    {
        protected static function booted(): void
        {
            static::saved(fn (Product $product) => Cache::forget("product:{$product->id}:summary"));
            static::deleted(fn (Product $product) => Cache::forget("product:{$product->id}:summary"));
        }
    }
  3. Group flush. With Redis or Memcached, cache tags let you clear a whole group:
    PHP
    Cache::tags(['catalog', "category:{$categoryId}"])->put($key, $value, 600);
    
    // When the category changes
    Cache::tags("category:{$categoryId}")->flush();
    You must read with the same tags you wrote with.

Model events do not fire for query-builder mass updates such as Product::where(...)->update([...]), so pair mass updates with an explicit forget or tag flush. To keep a hot value alive without rebuilding it, Laravel 13 adds Cache::touch($key, 3600), which extends the TTL of an existing item and returns false if it is missing.

Prevent stampedes with atomic locks

When a popular key expires, dozens of requests can rebuild it at the same moment and overload the database. An atomic lock lets one process do the work while the others wait briefly or fall back:

PHP
use Illuminate\Contracts\Cache\LockTimeoutException;

$report = Cache::get('report:monthly');

if ($report === null) {
    try {
        $report = Cache::lock('lock:report:monthly', 30)->block(5, function () {
            return Cache::remember('report:monthly', now()->addHour(), fn () => app(ReportBuilder::class)->build());
        });
    } catch (LockTimeoutException) {
        abort(503, 'Report is being generated, please retry shortly.');
    }
}

Inside the lock, remember checks again, so the processes that waited get the value the first one stored. Cache::flexible() also accepts an optional lock argument for its background refresh. For "only one at a time" work outside caching, Laravel 13 adds two helpers on the repository:

PHP
// One running instance across all servers
Cache::withoutOverlapping('sync:inventory', function () {
    // ...
}, lockFor: 120, waitFor: 5);

// At most three concurrent calls to a fragile API
Cache::funnel('partner-api')
    ->limit(3)
    ->releaseAfter(60)
    ->block(10)
    ->then(
        fn () => $client->pushOrders(),
        fn () => report('partner-api busy'),
    );

The Laravel 13 serialization change

New Laravel 13 applications set 'serializable_classes' => false in config/cache.php. Every cache store now unserializes values with PHP's allowed_classes option, so no PHP objects are restored from the cache. This blocks deserialization gadget-chain attacks if your APP_KEY or cache server is compromised.

The practical effect: if you cache Eloquent models, collections, Carbon dates or your own objects, they come back as __PHP_Incomplete_Class instead of the object you stored. You have two options:

  • Cache plain data (arrays, strings, integers), as the examples above do. This is the recommended approach and also makes cached values smaller and portable.
  • Allow specific classes by listing them, for example 'serializable_classes' => [App\Data\CachedDashboardStats::class].

Applications upgraded from Laravel 12 keep their old config file until you add the option, so check this when you upgrade.

Gotchas and what to do next

  • Tests: Laravel's phpunit.xml sets the cache to the array driver, so tag and lock behaviour in tests is not proof of Redis behaviour. Run one integration check against a real Redis.
  • Deferred refresh: flexible refreshes after the response through deferred functions, so a long rebuild still uses a PHP worker; keep rebuild work small or move it to a queued job.
  • Forever keys: anything stored with forever needs explicit invalidation, and Memcached may evict it anyway.
  • Measure: Cache::rememberWithWarmth() returns the value and whether it was a hit, which is handy for logging hit rates on key endpoints.

Next, list your ten slowest endpoints, decide for each whether its data is strict, slightly stale or group-invalidated, and apply remember, flexible or tags accordingly. Cache only what you can explain how to invalidate.

Frequently asked questions

  • Which cache driver should a Laravel 13 production app use?

    Redis is the usual choice for applications on more than one server because it supports tags and locks and is shared by every server. The default database driver is fine for small single-server apps.

  • What is the difference between Cache::remember and Cache::flexible?

    remember rebuilds the value as soon as its TTL expires, so one visitor waits. flexible takes fresh and stale durations and serves the stale value while refreshing it after the response, until the stale window ends.

  • Why do cached Eloquent models come back broken after upgrading to Laravel 13?

    New Laravel 13 apps set serializable_classes to false in config/cache.php, so objects are not unserialized. Cache arrays instead, or list the classes you trust in that option.

  • Can I use cache tags with the database or file driver?

    No. The Laravel 13 documentation states that tags are not supported on the file, dynamodb, database or storage drivers. Use Redis or Memcached, or versioned keys instead.

  • What does Cache::memo do?

    It wraps a store so repeated reads of the same key within one request or job are served from memory after the first read. Writes clear the memoized value and go to the real store.

  • How do I stop a cache stampede in Laravel?

    Wrap the rebuild in an atomic lock with Cache::lock()->block() and call remember inside it, or use Cache::flexible() so most requests receive the stale value during a refresh.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Cache (Laravel 13.x) — Laravel
  2. Upgrade Guide (Laravel 13.x) — Laravel
  3. Helpers: Deferred Functions (Laravel 13.x) — Laravel
  4. Cache Repository source, 13.x branch — Laravel (GitHub)
  5. laravel/laravel config/cache.php, 13.x branch — Laravel (GitHub)
  6. unserialize (allowed_classes option) — PHP Documentation

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.