Cache::remember
Returns a cached value, or runs the closure once and caches its result for the given lifetime.
Cache::remember(string $key, DateTimeInterface|DateInterval|Closure|int|null $ttl, Closure $callback): mixed- Added in Laravel 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$$key | string | The cache key to read and, on a miss, to write. |
$$ttl | DateTimeInterface|DateInterval|Closure|int|null | Lifetime in seconds, or a date/interval to expire at. Null stores the result indefinitely. A closure receives the computed value and returns the TTL. |
$$callback | Closure | Invoked only on a miss. Its return value is stored under the key and returned. |
Return value
mixedThe cached value on a hit, otherwise the closure's return value, which is written to the cache before being returned.
Errors
Anything the callback throws propagates and nothing is cached, so the next request runs the callback again.
Changelog
- PHP 5.8An integer TTL became seconds rather than minutes.
- PHP 11.xCache::flexible was added for stale-while-revalidate, recomputing in a deferred function instead of making one unlucky request wait.
Cache::remember is get-or-compute in one call, and it is the method most caching code should use. It reads the key; on a hit it returns immediately, on a miss it runs the closure, stores the result with the given TTL, and returns it.
When to reach for it
Any read path where the value can be derived from somewhere slower: an aggregate query, a report, a response from a third-party API, a parsed config file. Writing get then an if then put spreads the key and the TTL over three statements and invites them to drift apart.
Pass null as the TTL — or use rememberForever — when the value is only ever invalidated explicitly.
What trips people up
A closure that returns null is stored, but the next call cannot tell that null from a miss, so it runs the closure again on every request. If "nothing found" is a legitimate result you want to cache, return a wrapper such as ['value' => null].
remember is not atomic. When a hot key expires, every concurrent request misses and every one of them runs the closure — a cache stampede. Under real traffic use Cache::flexible() to serve stale data while one request refreshes, or guard the closure with Cache::lock().
An exception inside the closure caches nothing.
Example
<?php
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
// On a miss the closure runs and its result is cached for 300 seconds.
// On a hit the closure is never invoked.
$stats = Cache::remember('stats:orders', 300, function () {
return DB::table('orders')
->selectRaw('count(*) as orders, sum(total) as revenue')
->first();
});
print_r($stats);
// A null TTL stores the value indefinitely.
$countries = Cache::remember('countries', null, fn () => DB::table('countries')->pluck('name'));
echo $countries->count(), PHP_EOL;When to use it
- Cache an expensive reporting aggregate for five minutes on a dashboard many users hit.
- Hold a third-party API response so a rate-limited endpoint is called once per hour rather than per request.
- Memoize a permissions matrix that is rebuilt only when roles change.
Frequently asked questions
Why does my Cache::remember closure run on every request?
Almost always because the closure returns null, which remember cannot distinguish from a cache miss. Return a wrapper array or a sentinel value instead. The other cause is a TTL of zero or a negative number, which deletes the key immediately after writing it.
How do I stop many requests running the closure at once when the cache expires?
Use Cache::flexible($key, [$fresh, $stale], $callback), which serves the stale value and refreshes it in a deferred function after the response is sent. Cache::lock() around the recomputation also works and is stricter, at the cost of making some requests wait.
Does Cache::remember cache exceptions?
No. If the closure throws, nothing is written and the exception propagates, so the next request retries. That is usually what you want, but it means a failing dependency is hit on every single request rather than being backed off.
More examples
Why caching null silently does nothing
remember decides there was a hit by checking the retrieved value against null, so a cached null always looks like a miss. The counter shows the closure firing on every call — the exact failure people report as "negative caching does not work". Returning a one-key array makes the cached value non-null and the second block runs the closure once.
<?php
use Illuminate\Support\Facades\Cache;
$calls = 0;
$lookup = function () use (&$calls) {
$calls++;
return null; // "not found" is a real answer for us
};
Cache::remember('vat:XX999', 3600, $lookup);
Cache::remember('vat:XX999', 3600, $lookup);
Cache::remember('vat:XX999', 3600, $lookup);
echo "closure ran {$calls} times", PHP_EOL; // closure ran 3 times
// Wrap the result so the miss and the null are distinguishable.
$calls = 0;
$wrapped = function () use (&$calls) {
$calls++;
return ['value' => null];
};
Cache::remember('vat:XX999:v2', 3600, $wrapped);
Cache::remember('vat:XX999:v2', 3600, $wrapped);
echo "closure ran {$calls} times", PHP_EOL; // closure ran 1 timesPreventing a stampede on a hot key
The first call is the naive version: expiry is a synchronised event, so a hundred in-flight requests all become a hundred identical COUNT queries. flexible returns the stale value immediately and refreshes in the background, so no user waits. The lock variant is heavier but guarantees exactly one request computes while the others block for up to five seconds.
<?php
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
$query = fn () => DB::table('orders')->where('status', 'paid')->count();
// Plain remember: when the key expires, every concurrent request
// misses and every one of them runs the query.
$count = Cache::remember('paid:count', 60, $query);
// flexible: fresh for 60s, then served stale for up to 300s while
// a deferred function recomputes it after the response is sent.
$count = Cache::flexible('paid:count', [60, 300], $query);
// Or serialise the recomputation explicitly with a lock.
$count = Cache::get('paid:count') ?? Cache::lock('paid:count:lock', 10)->block(5, function () use ($query) {
return Cache::remember('paid:count', 60, $query);
});
echo $count, PHP_EOL;
Discussion