Cache Layers and Their Invalidation is the chapter about the layer teams add first and specify last. A cache is a rule about what may be wrong and for how long, and if nobody wrote the rule down, the cache is not a performance feature. It is an undocumented correctness change.
Key takeaways
- Laravel configures a fresh application to use the
databasecache driver, so a team that assumed Redis may be caching into the database it was trying to protect.- Cache tags are unavailable on the
file,dynamodb,databaseandstoragedrivers, andCache::flushdoes not respect the configured cache prefix and removes all entries.Cache::flexibletakes a fresh duration and a stale duration: inside the stale window it returns the old value immediately and registers a refresh that runs after the response has been sent.- Redis documents
maxmemoryas defaulting to zero, meaning no limit, on 64-bit systems, and its eviction page does not state a defaultmaxmemory-policy, so both are decisions you own.- Cache health reads as
keyspace_hits / (keyspace_hits + keyspace_misses) * 100, withevicted_keysdistinguishing a policy problem fromexpired_keys, which indicates a TTL that is too short.
Read this after Chapter 2, because a stampede is a queue rather than a cache miss, and after Chapter 5, because a cache is often how teams avoid a lock they could have narrowed instead. Chapter 9 handles the same staleness question one layer out, where the caches belong to browsers and shared proxies rather than to you.
A dashboard query takes seconds, so somebody wraps it in a cache with a five-minute TTL. It works. The page is fast for a quarter.
Then the entry expires during peak. Forty requests arrive in the same second, find nothing, and all forty run the seconds-long query at once. The database saturates, the requests time out, nothing gets written back, and the next second does it again.
The cache did not fail. It expired, which is the thing caches do.
A cache is a written rule about what may be stale
Before any driver or TTL, a cache needs a sentence per key, and the sentence has three parts. What this key holds, how wrong it is allowed to be, and who notices when it is wrong.
That sounds like paperwork until you try to write it for a live application. Most keys turn out to fall into two groups. Values where staleness is invisible, such as a navigation tree or a country list. And values where staleness is a support ticket, such as a balance, a stock count or a permission.
Those two groups deserve different machinery, and mixing them is the origin of most cache incidents I have seen. A single TTL applied to both means either the safe keys are refreshed pointlessly or the dangerous ones go stale silently.
So write the rule first. The driver choice follows from it.
A fresh Laravel application caches in the database
This is the detail that surprises teams who assumed otherwise. Laravel's cache documentation
states that by default Laravel is configured to use the database cache driver, storing
serialised cached objects in your application's database.
Read the consequence carefully. If the thing you were protecting was the database, and you cached into the database, you added a write path and a read path to the resource under pressure. It still helps, because a keyed lookup is cheaper than an aggregate. It helps less than everyone assumed, and it fails at the same time as the thing it protects.
The useful mental model is a ladder of layers, each faster and each narrower in scope. In-request
memoisation, which Laravel exposes as the memo driver, is the fastest and lives for one
request. The Octane cache lives in one server's memory and Chapter 8 explains what that costs.
A shared store like Redis or Memcached lives across processes. The database and file drivers are
the durable, slow end.
Pick the highest layer that satisfies the rule you wrote. A value that only needs to be consistent within one request never needs a network hop.
Tags are unavailable exactly where you would want them
Cache tags are the feature people design invalidation around, and Laravel's documentation is
clear that they are not universally available. Tags are not supported on the file, dynamodb,
database or storage drivers.
Since database is the default driver, a team that designs a tag-based invalidation scheme
without changing drivers has designed something that does not run. It throws on the first call.
That is the good case.
The slower failure is Cache::flush. Laravel documents that flushing does not respect the
configured cache prefix and removes all entries from the cache. On a shared Redis instance
serving several applications, or serving your queues and your sessions, that is a much larger
blast radius than the method name suggests.
The rule I hold to: never call a global flush from application code. Invalidate by key, or by tag on a driver that supports tags, or by writing a new key and moving a pointer.
The stampede is a queue, not a cache miss
Chapter 2's model explains the failure in the scene above better than any cache vocabulary does. At the moment of expiry, arrival rate is unchanged and service time jumps from a cache read to a full recomputation. Utilisation goes past one instantly.
There are two structural answers and Laravel ships both. The first is to make only one caller
recompute. Laravel's atomic locks do that, and its documentation records the constraint: locks
require the application default cache driver to be memcached, redis, dynamodb, database,
file or array, and all servers must communicate with the same central cache server. The
withoutOverlapping and funnel helpers wrap the same primitive in shapes that fit jobs and
tasks.
The second is to make nobody wait. That is what a stale window is for, and it is the subject of the next section.
Both are better than a longer TTL. A longer TTL delays the stampede and makes it bigger, because more callers accumulate behind a colder, more expensive recomputation. Expiry is not the bug. The thundering recompute is.
Cache::flexible gives you three windows
Laravel's Cache::flexible method takes an array with two durations, a fresh duration and a
stale duration, and produces three distinct behaviours from one call.
Inside the fresh window, the cached value is returned. Inside the stale window, the old value is returned immediately and a refresh is registered that runs after the response has been sent to the user, so nobody waits for it. Past the stale window the value is recomputed inline and the caller does wait.
That pattern has a lineage worth knowing, because it makes the design intent legible. RFC 5861,
published by the RFC Editor in May 2010 as an Informational document, defined
stale-while-revalidate and stale-if-error for HTTP caches. Laravel's flexible cache is the
same idea applied to an application-level store, and Chapter 9 uses the HTTP original.
Laravel 13 adds two neighbours. Its release notes for 17 March 2026 name Cache::touch, which
extends an existing item's time to live without retrieving and re-storing the value, and the
framework also documents rememberWithWarmth for warming an entry ahead of its expiry.
The decision rule is short. If a value can be one refresh interval out of date without anybody being harmed, it belongs in a flexible cache rather than a plain one.
Eviction policy is a decision, not a default you can assume
A cache with no memory limit and no eviction policy is not a cache. Redis's documentation gives you the exact reason, and it is a documented default rather than an opinion.
Setting maxmemory to zero means no limit on the dataset, and Redis's eviction page states that
zero is the default behaviour on 64-bit systems, with 32-bit systems using an implicit three
gigabyte limit. So an unconfigured 64-bit Redis grows until something else on the machine loses.
The same page notes that replication and persistence buffers are excluded from the total compared
against maxmemory, reported as mem_not_counted_for_evict, which is why you leave headroom.
The maxmemory-policy directive selects the policy. The available names are noeviction, then
lru, lrm, lfu and random in both an allkeys and a volatile family, plus
volatile-ttl. Least Recently Modified is the newest of them, available starting with Redis 8.6,
and it updates its timestamp only on writes rather than on reads.
Three documented details change how you read the graphs. The volatile policies behave like
noeviction when no key has an expiration set. Redis LRU is an approximation that samples keys,
tuned by maxmemory-samples. And LFU uses a Morris counter that Redis documents as saturating
at around one million requests and decaying every minute by default, tuned with lfu-log-factor
and lfu-decay-time.
Redis's own rule of thumb, on that page, is that allkeys-lru is a good default when you expect
a subset of keys to be accessed far more often than the rest. That is their recommendation, not a
measurement of mine.
Write down what may go stale, and for how long
This is the artifact the chapter exists to produce. One table, kept in the repository, one row per cached thing.
| Cached value | Allowed staleness | Mechanism | Who notices if wrong |
|---|---|---|---|
| Navigation and taxonomy | Hours | Long TTL, flexible window | Nobody, until a term is renamed |
| Aggregate dashboard counts | Minutes | Flexible, stale window covers refresh | An analyst, mildly |
| Permission or role checks | Zero | Not cached, or invalidated on write | A user, immediately, as a security event |
| Account balance | Zero for the owner | Read through, or per-request memoisation only | The customer, loudly |
| Rendered public page fragment | Minutes | Tagged cache, invalidated on publish | An editor, within one refresh |
The middle row is the one worth arguing about in a room. Permissions are cached in more applications than anybody admits, and the reason is always that permission resolution was slow. Chapter 3 is the right place to fix that, not this chapter.
A worked reading, composited and labelled
The details here are composited from ordinary production shapes rather than one system, and the shape is exact.
Cache effectiveness is reported as good because the hit ratio is high. The first reading computes
that ratio from Redis's own counters, as keyspace_hits over hits plus misses, and pairs it with
evicted_keys and expired_keys on the same axis. High hits with climbing evictions is a
different diagnosis from high hits with climbing expiries.
Redis's documentation gives you the interpretation directly. A high proportion of evictions
suggests the wrong keys are being evicted by the chosen policy. A low eviction count with a high
expired_keys count suggests the TTL is too short, or that the wrong keys were given one.
The second reading is the same three series after the change, whether that change was a policy, a memory limit or a TTL. What you are looking for is a shift in which counter moves, not a percentage. The ratio alone would have told you nothing, which is the point.
The objection: just add Redis
Adding a fast shared store is a good move that gets asked to do a bad job. Two of those jobs are worth naming, because both are common and both fail late.
The first is caching a slow dependency to avoid fixing it. A cached response to a third-party API that takes four seconds does not make the dependency fast, it makes the first caller of every window absorb it. The fix belongs at the call site, with a timeout and a fallback, and Chapter 7 covers the retry contract that goes with it.
The second is treating the cache as available. It is a network service with its own capacity, and
Laravel documents a failover cache driver that moves to another store and dispatches a
CacheFailedOver event when the first one is unreachable. Configure that deliberately, and decide
what your application does when the cache is gone rather than discovering it.
There is a smaller point that matters under high call volume. Laravel documents that cache events can be disabled per store, which is worth doing on a store you call thousands of times per request cycle, because every interaction otherwise dispatches an event nobody is listening to.
Chapter summary
A cache is a written rule about what may be wrong and for how long, with a named party who
notices, and the two groups of value, invisible staleness and support-ticket staleness, need
different machinery rather than a shared TTL. Laravel defaults a fresh application to the
database cache driver, which means a team caching to protect the database may be caching into
it, and the useful model is a ladder from per-request memoisation through the Octane cache to a
shared store and finally the durable drivers. Tags are unavailable on file, dynamodb,
database and storage, and Cache::flush ignores the configured prefix and removes every
entry, which on a shared Redis is a far wider blast radius than the name implies. A stampede is
Chapter 2's queue arriving at the instant of expiry, answered either by letting one caller
recompute under an atomic lock, which requires a central cache server and one of the documented
drivers, or by nobody waiting at all, which is what Cache::flexible provides through a fresh
window, a stale window whose refresh is deferred until after the response is sent, and an expired
state that recomputes inline. That pattern descends from RFC 5861, published May 2010, which
defined stale-while-revalidate and stale-if-error. Redis documents maxmemory as unlimited
by default on 64-bit systems and states no default eviction policy, so an unconfigured instance
grows until something else loses, and the diagnostic pair is evicted_keys against
expired_keys beside the hit ratio. And adding Redis does not fix a slow dependency; it moves
the cost to whoever arrives first in each window.
Deferring work until after the response is exactly what the stale window does, and the general version of that move is a queue. Chapter 7 is Queues, Workers, and Horizon Under Load, where the pool from Chapter 2 has a longer service time and a much longer memory.
Sources
- CacheLaravel · 2026-07-29 · Official documentation · verified
- Key evictionRedis · 2026-07-29 · Official documentation · verified
- RFC 5861: HTTP Cache-Control Extensions for Stale ContentRFC Editor · 2010-05 · Standard · verified
- Release Notes, Laravel 13Laravel · 2026-03-17 · Official documentation · verified