illuminate/cache
The Illuminate Cache package provides Laravel's caching system, with a unified API for file, database, Redis, Memcached, DynamoDB, and array stores. It supports cache tags, atomic locks, rate limiting, and flexible serialization for fast, reliable data storage and retrieval across applications.
illuminate/cache is already installed in any Laravel app — it's a subtree split of laravel/framework, so don't add it separately unless you're using it outside Laravel (or in a package that must support both). If you do need it standalone:
composer require illuminate/cache illuminate/container illuminate/config illuminate/filesystem
Pin the major version to match your framework version (^11.x, ^12.x); the split publishes on the framework's release cadence and there is no independent changelog.
Where to look first
config/cache.php — the default store, the stores map, and prefix. This is the only file most developers ever touch.Illuminate\Cache\CacheManager — resolves a driver string to a Repository; Cache::store('redis') is a call into here.Illuminate\Contracts\Cache\Repository — the ~15 methods you actually use (get, put, remember, add, pull, increment, lock). Read this interface once and you know the whole API surface.Illuminate\Cache\ArrayStore — the reference implementation. It's the fastest way to understand key/TTL semantics without touching disk or network.Standalone bootstrap (only if you're outside Laravel — this is the main friction point of the package):
use Illuminate\Cache\CacheManager;
use Illuminate\Container\Container;
use Illuminate\Config\Repository as Config;
use Illuminate\Filesystem\Filesystem;
$c = new Container();
$c->instance('config', new Config([
'cache.default' => 'file',
'cache.stores.file' => ['driver' => 'file', 'path' => __DIR__.'/cache'],
'cache.prefix' => 'myapp',
]));
$c->instance('files', new Filesystem());
$cache = (new CacheManager($c))->store();
First use case — wrap the one expensive thing on your critical path and measure:
$stats = Cache::remember('dashboard:stats:v1', now()->addMinutes(5), function () {
return DB::table('orders')->selectRaw('status, count(*) as n')->groupBy('status')->get();
});
Then confirm it's actually hitting: Cache::getStore() should be your configured driver, not ArrayStore. If it's ArrayStore, you're in a test or a misconfigured container.
Cache-aside behind a service class. Don't scatter Cache:: calls through controllers. Wrap each cached concern in one class with a single key-builder, so TTLs, key names, and invalidation live in one place and are mockable:
final class ArticleCache
{
public function latest(int $limit = 10): Collection
{
return Cache::remember(
$this->key("latest:{$limit}"),
now()->addMinutes(10),
fn () => Article::latest()->limit($limit)->get()
);
}
public function bust(): void { Cache::forget($this->key('latest:10')); }
}
Key naming and versioning. Adopt app:entity:id:version (shop:product:42:v3). Bumping the version is the cheapest mass-invalidation you'll ever write — cheaper and safer than flush() and it works on every store. Never interpolate raw user input into a key without hashing; keys with spaces, {, }, or >250 bytes break Memcached hard.
Tagged invalidation where the store supports it. Cache::tags(['articles', 'articles:42'])->remember(...) plus Cache::tags(['articles'])->flush() is the cleanest invalidation story — but only on TaggableStore implementations (Redis, Memcached, Array). Check Cache::getStore() instanceof TaggableStore before relying on it, or you'll ship a file-store app that throws in production.
Stampede protection. remember() doesn't protect a hot key from a thundering herd on expiry. Combine a lock with a double-check:
$value = Cache::get($key);
if ($value === null) {
$value = Cache::lock("{$key}:lock", 10)->block(5, function () use ($key, $ttl, $callback) {
return Cache::remember($key, $ttl, $callback);
});
}
For read-heavy endpoints on Laravel 11+, Cache::flexible($key, [60, 300], $callback) gives you stale-while-revalidate and sidesteps the stampede entirely.
Atomic primitives instead of get-then-put. Cache::add() (set-if-absent), Cache::increment()/decrement(), Cache::pull() (get-and-forget), and Cache::lock() all map to single backend operations on Redis/Memcached/Database. Use them for counters, one-time tokens, onboarding flags, and rate limits. add() returning false is your "someone else got here first" signal — that's the whole point.
Per-call store selection. Cache::store('redis')->put(...) for things that must survive a deploy, while the default file store handles cheap page fragments. Mixing is normal; document which is which. A common split is Redis for locks/counters/sessions, file for local fragments, array for request-scoped memoization.
Warming and inspection as artisan commands. Caches that only fill on first request mean your first user after a deploy eats the latency. Add a cache:warm command that calls the same ArticleCache::latest() methods, and schedule it post-deploy. This is also where you discover keys that never get hit.
Custom stores. Implement Illuminate\Contracts\Cache\Store (six methods: get, put, increment, decrement, forever, forget, plus flush) and register via Cache::extend('dynamodb', fn ($app) => ...). Do this only when you have a genuine persistence layer need — the round-trip cost of the contract is low, but you own all serialization and TTL semantics.
Testing. ArrayStore is the default in phpunit.xml (CACHE_STORE=array), which means tests get a clean, isolated cache per test — use it. For assertions, Cache::spy() and Cache::shouldReceive('remember') let you verify the caching contract rather than the cached value.
remember() and null. The implementation is if (! is_null($value = $this->get($key))) return $value; — so if your callback legitimately returns null, the key is written but every subsequent call re-executes the callback. For "known empty" results, cache a sentinel (['__none__'] or an empty collection) instead of null.0/null trap. TTLs are seconds (or a DateTimeInterface); 0 and null both mean forever, and a DateTimeInterface in the past deletes the key immediately. Prefer now()->addMinutes(5) over bare integers — it survives a refactor to a different store. Note that ArrayStore ignores TTL entirely, so a test suite on array will never catch an expired-key bug.false is a value, not a miss. Cache::put('flag', false) and a missing key both return false from get(). Disambiguate with Cache::has() / Cache::missing() when the distinction matters.__PHP_Incomplete_Class after a deploy, you renamed or deleted a class that's still in the cache; version your keys.flush() is a footgun on Redis. It clears the entire database, not just your keys. Always set CACHE_PREFIX per application, and prefer versioned keys or tag flushes over flush(). Same caution for php artisan cache:clear — it only clears the default store, so tagged or secondary stores survive and confuse you.storage/framework/cache/data; look for failed writes in storage/logs/laravel.log, and verify with Cache::put('probe', 1, 60) followed by Cache::has('probe'). Directory permission issues are the #1 "cache doesn't work" cause in local dev.increment/decrement are not atomic everywhere. File and database stores approximate it with read-modify-write; concurrent requests will lose updates. Use Redis (or DatabaseStore, which does it in a transaction on supported versions) for real counters.Cache::lock() on the file driver is a best-effort advisory lock. For anything you'd call a mutex — payment processing, job deduplication — the lock store must be Redis/Memcached/Database. Always pass an explicit $timeout and prefer the block(seconds, callback) form so the lock is released on exception.CACHE_DRIVER to CACHE_STORE. If you're following older docs or a package's .env.example, you'll silently get the file default with no error — everything "works," just slower than you think.CacheHit, CacheMissed, KeyWritten, KeyForgotten) or just Cache::listen(fn ($e) => logger(class_basename($e))) from a service provider to get a hit/miss trace without a profiler. Pair that with DB::listen to see whether cached code is still querying — that's the real signal that a key is missing or being rebuilt every request.ArrayStore and other in-memory stores can leak state across requests in surprising ways. Anything request-scoped must be reset in a listener or avoided entirely; use Redis for shared state.Cache::extend() for new drivers, Cache::macro() for app-wide helpers, and the Repository decorator pattern if you want to add key logging or hit-ratio metrics without touching call sites. Everything else is deliberately non-extensible — resist subclassing CacheManager.How can I help you explore Laravel packages today?