In Laravel, how does Cache::remember() cache an expensive exchange-rate lookup, and what happens on a cache hit versus a miss?
answer
- closure runs only when the key is absent
- TTL: integer seconds or a DateTime
- null from the closure is never a hit
- put() with no TTL stores forever
- add / pull / forget / rememberForever
basics
~20 sCache::remember($key, $ttl, $closure) returns the cached value when the key exists; on a miss it runs the closure, stores the result for the TTL (integer seconds or a DateTime) and returns it, so the expensive lookup runs once per TTL window.
solid answer
~40 s`Cache::remember('fx:rates:EUR', 600, fn () => $client->latest('EUR'))` first calls `get()`. If the store returns a non-null value that is a **hit** and the closure never runs. On a **miss** it runs the closure, calls `put()` with the TTL and returns the fresh value. An integer TTL is seconds; a `DateTimeInterface` or `DateInterval` works too. Two traps: a closure that returns `null` is treated as a miss on every call, because Laravel uses `null` to mean "not cached"; and `remember()` takes no lock, so concurrent misses each run the closure. The same API family covers `add()` (write only if absent), `pull()` (read then delete), `forget()` and `rememberForever()`.
code
php · 22 lines<?php
use Illuminate\Support\Facades\Cache;
final class ExchangeRates
{
public function __construct(private RateProvider $provider) {}
public function for(string $base): array
{
return Cache::remember(
"fx:rates:{$base}",
now()->addMinutes(10),
fn () => $this->provider->latest($base) ?? [], // never cache null
);
}
public function invalidate(string $base): void
{
Cache::forget("fx:rates:{$base}");
}
}go deeper
Recall the signature remember(key, ttl, closure), that integer TTLs are seconds, and that the closure only runs on a miss. Know get, put, forget and rememberForever by name.
Walk through the get, null check, closure and put sequence, explain why a null result is never a hit, and contrast add, pull and has with plain get and put.
Point out that remember takes no lock, so a hot key expiring causes a stampede, and name flexible or a lock as the fix. Insist that keys encode every input.
Frame which data deserves forever versus a short TTL, who owns invalidation when the rate provider corrects a value, and when per-request memo beats another store round-trip.
## What `Cache::remember()` is for Laravel's cache API lives behind the `Illuminate\Support\Facades\Cache` facade (and the global `cache()` helper). Both resolve a **repository** that wraps the configured **store** — in a Laravel 13 skeleton that is the `database` store, set by `CACHE_STORE=database` in `.env`. `remember()` is the one-call version of the classic *check the cache, otherwise compute and save* sequence. On a currency-converter site, fetching exchange rates from an external provider can take hundreds of milliseconds and may be rate-limited, so you want it to happen once per few minutes, not once per page view. ```php $rates = Cache::remember("fx:rates:{$base}", 600, function () use ($base) { return $this->provider->latest($base); // slow HTTP call }); ``` ## Hit versus miss, step by step The framework's `Repository::remember()` delegates to `rememberWithWarmth()`, which does exactly this: 1. Call `get($key)`. The store looks the key up (with the configured prefix applied) and fires a `CacheHit` or `CacheMissed` event. 2. If the value is **not `null`**, return it immediately — a hit. The closure is never invoked. 3. Otherwise run the closure, call `put($key, $value, $ttl)` and return the value — a miss. `rememberWithWarmth()` itself returns `[$value, $warm]`, so you can tell a hit from a miss when you want to log or measure it. The TTL deserves precision: - An **integer** is a number of **seconds**. - A `DateTimeInterface` (for example `now()->addMinutes(10)`) or a `DateInterval` sets an absolute or relative expiry. - The TTL argument may itself be a closure that receives the computed value, so the lifetime can depend on the data. - A TTL of **0 or less** makes `put()` delete the key instead of writing it. - `put()` with **no TTL** (`null`) stores the item **forever**, exactly like `forever()`. ## The rest of the everyday API | Call | What it does | Returns | |---|---|---| | `Cache::get($key, $default)` | Reads a key; `$default` may be a closure | value or default (`null` if none) | | `Cache::put($key, $value, $ttl)` | Writes unconditionally | `bool` | | `Cache::add($key, $value, $ttl)` | Writes **only if the key is absent** | `true` if it wrote | | `Cache::pull($key, $default)` | Reads, then deletes | value or default | | `Cache::forget($key)` | Deletes one key | `bool` | | `Cache::rememberForever($key, $closure)` | Like `remember()` with no expiry | value | | `Cache::has($key)` | `false` for missing **and** for a stored `null` | `bool` | | `Cache::touch($key, $ttl)` | Extends an existing item's TTL (new in Laravel 13) | `false` if missing | `add()` is documented as atomic, which makes it useful for "first writer wins" counters and flags; the database store implements it with an insert-or-ignore on most connections. ## Traps interviewers probe - **`null` is never cached as a hit.** If the provider returns `null` for an unknown currency, every call re-runs the closure. Cache a sentinel (an empty array, `false`) or validate the input first. - **No lock on a miss.** When a hot key expires, every concurrent request that misses runs the closure. That is a stampede; Laravel's answers are `Cache::flexible()` (serve stale, refresh in the background) and atomic locks. - **The key must encode every input.** `fx:rates` for all base currencies would serve EUR rates to a USD visitor. Build keys from the parameters: `"fx:rates:{$base}"`. - **Forever means until something deletes it.** `rememberForever()` entries survive deploys; you need an explicit `forget()` or a flush when the data changes. - **Objects in the payload.** A Laravel 13 skeleton sets `serializable_classes` to `false` in `config/cache.php`, so cache arrays or scalars rather than arbitrary objects unless you allow-list their classes. ## Reading the same key many times in one request `remember()` still costs one store round-trip per call. If a request reads `fx:rates:EUR` from several places, `Cache::memo()->get(...)` decorates the store with a per-request (or per-job) in-memory layer: the first read hits the store, later reads come from memory, and any write through the memo repository forgets the memoized copy. ## `remember()` versus writing it by hand The hand-written version is longer and easier to get subtly wrong: ```php $rates = Cache::get("fx:rates:{$base}"); if ($rates === null) { $rates = $this->provider->latest($base); Cache::put("fx:rates:{$base}", $rates, 600); } ``` It behaves the same as `remember()` — including the `null` trap and the lack of locking — but repeats the key twice, which invites typos where the read and the write use different keys. `remember()` also works identically through the `cache()` helper (`cache()->remember(...)`) and on a named store (`Cache::store('redis')->remember(...)`). Reach for the manual form only when the write needs a different TTL or store than the read, or when you must inspect the fresh value before deciding to cache it at all, for example refusing to cache an error payload from the rate provider.
- In Laravel, why would a Cache::remember() closure that returns null run on every request?The repository treats `null` from the store as "not cached": `remember()` only returns early when `get()` yields a non-null value. A closure returning `null` is written, then read back as a miss next time, so the expensive call repeats. Return a sentinel such as an empty array or `false`, or skip caching invalid inputs.
- In Laravel 13, how do you extend the lifetime of a hot cached entry without recomputing or rewriting it?`Cache::touch('fx:rates:EUR', 3600)` (new in Laravel 13) resets the item's TTL in the store without fetching and re-storing the value. It accepts seconds, a `DateTimeInterface` or a `DateInterval`, returns `false` when the key does not exist, and deletes the key when the computed TTL is zero or negative.
- In Laravel, what does Cache::memo() add when the same key is read many times within one request?`Cache::memo()` wraps a store in a memoizing repository that is scoped to the current request or job. The first `get()` for a key goes to the real store; repeated reads return the in-memory copy. Mutating calls such as `put()` or `increment()` forget the memoized value and go through to the underlying store.
saying these in an interview costs you the question
- Cache::remember() locks the key so only one request runs the closure
- An integer TTL passed to Cache::remember() is measured in minutes
- Cache::put() without a TTL falls back to a default expiry from config
- A null result is cached, so the next call returns it without running the closure
- Cache::get() throws an exception when the key does not exist