Introduction
Caching can make a Laravel app feel instant—until stale data, thundering herds, or over-broad flushes turn your “speed boost” into a reliability problem. If you’ve ever asked when to use Cache::remember()
versus Cache::rememberForever()
, you’re in the right place. This guide demystifies Laravel cache remember vs rememberForever, then goes beyond basic definitions to show how to keep your data fresh with cache invalidation patterns in Laravel that scale.
We’ll start with a pragmatic decision framework to choose between time-bound caching and permanent entries. From there, you’ll learn how to design keys you can actually invalidate, structure list and item caches for real-world endpoints, and use Laravel cache tags to surgically flush related data without nuking your entire cache. Finally, we’ll tackle production headaches like stampedes and race conditions, including locks, versioned key namespaces, and early-refresh strategies to prevent cache stampede in Laravel.
Whether you’re optimizing a read-heavy API, stabilizing dashboards and reports, or reducing database pressure during peak traffic, the patterns here are designed to be drop-in, testable, and easy to reason about. By the end, you’ll have a practical playbook for choosing remember
or rememberForever
with confidence—and a set of invalidation techniques that keep your cache fast, correct, and boring (in the best possible way).
Table of contents
- Why application caching fails (and how to fix it)
- The difference between
remember
andrememberForever
- Quick decision framework: which one should you use?
- Designing keys that are easy to invalidate
- Invalidation patterns that actually work
- Cache-aside (delete-on-write)
- Write-through
- Tag-based invalidation
- Versioned keys (namespacing)
- Event-driven invalidation
- Index + item (for lists)
- Preventing cache stampedes and race conditions
- TTL strategy: how long is “long enough”?
- Driver nuances: Redis, Memcached, file, database
- Observability: knowing when your cache saves you
- Common pitfalls and how to avoid them
- End-to-end examples
- FAQs
1) Why application caching fails (and how to fix it)
Most Laravel teams adopt caching to reduce database load or expensive API calls, then hit three classic failure modes:
- Stale data that persists longer than it should because invalidation is manual or forgotten.
- Thundering herds when a hot key expires and many workers recompute it simultaneously.
- Over-broad flushing (e.g., nuking the entire cache) that erases good data alongside stale entries.
This article focuses on Laravel cache remember
vs rememberForever
—when to use each—and a set of cache invalidation patterns in Laravel that are predictable, testable, and resilient.
2) The difference between remember
and rememberForever
Laravel’s Cache façade offers two workhorse methods:
// remember: caches for a bounded time (TTL)
Cache::remember($key, $ttl, function () {
return computeExpensiveThing();
});
// rememberForever: caches until explicitly invalidated
Cache::rememberForever($key, function () {
return computeExpensiveThing();
});
remember($ttl)
- Stores the value with an explicit time-to-live.
- Ideal when the data changes periodically or you’re okay with bounded staleness.
- Old values self-expire, which reduces the risk of “forgotten invalidation.”
rememberForever()
- Stores the value until you evict it via
Cache::forget()
ortags()->flush()
. - Best for data that is effectively static or has deterministic change triggers (admin action, deployment, config publish).
- Requires a reliable invalidation mechanism; otherwise, you will serve stale content indefinitely.
- With Redis/Memcached, “forever” means no TTL, but LRU eviction can still drop entries under memory pressure.
Bonus: Cache::sear()
is a helper for “cache forever if missing,” functionally similar to rememberForever
.
3) Quick decision framework: which one should you use?
Ask three questions:
- How often does the underlying data change?
- Rarely and predictably →
rememberForever
is safe (with a clear invalidation hook). - Occasionally or unpredictably →
remember($ttl)
.
- Rarely and predictably →
- What’s the cost of being stale?
- Low (e.g., a list of countries) →
rememberForever
. - Medium/High (e.g., pricing, quotas, inventory) →
remember
with a short but practical TTL plus event-based invalidation when possible.
- Low (e.g., a list of countries) →
- Do you have a deterministic invalidation trigger?
- Yes (observer, admin action, deploy) →
rememberForever
is fine. - No → prefer
remember($ttl)
.
- Yes (observer, admin action, deploy) →
Rule of thumb: Default to remember
unless you can name the exact line of code that will invalidate a rememberForever
entry.
4) Designing keys that are easy to invalidate
Good keys make invalidation easier:
- Namespace by resource and variant:
users:profile:{id}:compact
,posts:list:{hash(filters)}:p{page}
- Encode inputs:
Hash the query string or filter JSON so keys reflect the dataset precisely. - Keep them short:
Aim for <200 characters (especially for Memcached). - Be consistent:
Decide on:
vs.
separators and stick to it for grep-ability and tooling.
5) Invalidation patterns that actually work
A) Cache-aside (delete-on-write)
Pattern: Read through cache; on writes, delete the affected key(s) so the next read repopulates it.
Read path:
$user = Cache::remember("users:{$id}", now()->addMinutes(15), function () use ($id) {
return User::findOrFail($id);
});
Write path:
$user->fill($data)->save();
Cache::forget("users:{$user->id}");
Why it works: Simple, predictable, and plays well with remember($ttl)
or rememberForever
(if you’re disciplined about forget()
).
When to prefer: Most CRUD contexts with moderate read traffic.
B) Write-through (update cache immediately)
Pattern: After saving, write the new value to cache directly to avoid a cold miss.
$user->fill($data)->save();
Cache::put("users:{$user->id}", $user->fresh(), now()->addMinutes(15));
Pros: Smooth read path after writes; fewer cold misses.
Cons: Slightly more code; you must keep TTLs consistent with the read path.
C) Tag-based invalidation (group flushes)
Pattern: Assign tags to related keys so you can invalidate by group.
// read
$user = Cache::tags(['users', "user:{$id}"])
->remember("users:{$id}", now()->addHour(), fn () => User::findOrFail($id));
// invalidate a single user subset
Cache::tags(["user:{$id}"])->flush();
Notes:
- Requires a driver that supports tags (Redis, Memcached). Not supported by file or database drivers.
- Pick narrow tags (e.g.,
user:{id}
) to avoid carpet-bombing the cache.
When to prefer: Complex aggregates, many related keys, or when multiple views depend on the same entity.
D) Versioned keys (a.k.a. key namespacing)
Pattern: Store a version counter per resource and include it in the key. Increment the version to invalidate without deleting.
$ver = Cache::rememberForever("users:{$id}:ver", fn () => 1);
$key = "users:{$id}:v{$ver}";
$user = Cache::remember($key, now()->addMinutes(30), fn () => User::findOrFail($id));
On write:
Cache::increment("users:{$id}:ver");
Readers naturally move to the new key space; old keys expire in place.
When to prefer: Hot keys prone to stampede, or multi-layer caching where deletes are expensive.
E) Event-driven invalidation (Observers)
Pattern: Hook into model events to centralize cache hygiene.
// App/Observers/UserObserver.php
class UserObserver
{
public function saved(User $user) { Cache::forget("users:{$user->id}"); }
public function deleted(User $user) { Cache::forget("users:{$user->id}"); }
}
// AppServiceProvider
User::observe(UserObserver::class);
When to prefer: Clean separation of concerns; reduces missed invalidations during refactors.
F) Index + item (for lists)
Pattern: Cache two layers: an index (IDs for a query signature) and each item by ID. On writes, refresh the item and bump/forget affected indexes.
// List
$signature = md5(json_encode(request()->query()));
$indexKey = "posts:index:{$signature}:p".(int)request('page', 1);
$ids = Cache::remember($indexKey, 600, function () {
return Post::published()
->latest()
->paginate(20)
->pluck('id')
->all();
});
// Items
$posts = collect($ids)->map(function ($id) {
return Cache::remember("posts:item:{$id}", 600, fn () => Post::find($id));
});
On create/update/delete:
- Refresh
posts:item:{id}
orforget
it. - Forget/bump index keys only for list signatures known to be affected (e.g., by tag or a derived index registry).
Why it works: You avoid flushing huge serialized lists when only one item changes.
6) Preventing cache stampedes and race conditions
A cache stampede happens when a hot key expires and many workers regenerate it simultaneously. To prevent cache stampede in Laravel:
- Locks for expensive builders
$key = 'reports:monthly:2025-09';
$data = Cache::remember($key, 3600, function () use ($key) {
return Cache::lock("lock:{$key}", 10)->block(10, function () {
return computeHeavyReport();
});
});
2. Early refresh / soft TTL
Store data
plus refreshed_at
. If the hard TTL hasn’t passed but “freshness” is nearing a threshold, serve cached data and queue a refresh.
3. Jittered TTLs
Add small randomness to TTLs to avoid synchronized expiry:
$ttl = now()->addMinutes(10 + random_int(0, 2));
4. Versioned keys
Invalidate by moving readers to a new key (increment
a version namespace) instead of deleting a hot key.
7) TTL strategy: how long is “long enough”?
Think in terms of freshness budget and cost-of-stale:
- Static reference data: hours to days, or
rememberForever
with a clear invalidation. - User profiles: 10–30 minutes; event-driven invalidation on edit.
- Search results / lists: 5–15 minutes, plus index/item pattern.
- Dashboards / reports: 5–60 minutes + lock + early refresh.
Guiding rule: If a human will notice stale data within minutes, keep TTL ≤ the tolerance and supplement with event-driven invalidation.
8) Driver nuances: Redis, Memcached, file, database
- Redis
- Fast, persistent (RDB/AOF), supports tags and atomic ops (
increment
, locks). - Best default for production.
- Fast, persistent (RDB/AOF), supports tags and atomic ops (
- Memcached
- Very fast, volatile (RAM only), supports tags, LRU eviction may drop “forever” keys under pressure.
- File
- Simple but no tags, slower for many small keys, not ideal beyond local dev.
- Database
- Works but no tags; can become a bottleneck if misused.
Pick a driver that supports the features your invalidation strategy needs—if you plan to lean on tags, avoid file/database stores.
9) Observability: knowing when your cache saves you
Add basic metrics and logging:
- Hit/miss counters per namespace (
users
,posts
,settings
). - Rebuild latency for heavy computations—alert on regressions.
- Key cardinality (e.g., number of active list signatures) to detect cache bloat.
- Evictions and memory pressure (Redis INFO, Memcached stats).
Even a few counters in Prometheus or logs can prevent blind spots.
10) Common pitfalls and how to avoid them
- Using
rememberForever
without a delete hook- Fix: Add an observer or admin action that calls
forget()
ortags()->flush()
.
- Fix: Add an observer or admin action that calls
- Over-broad tag flushes
- Fix: Prefer granular tags like
user:{id}
; reserveusers
for rare global rebuilds.
- Fix: Prefer granular tags like
- Caching entire Eloquent collections with relations attached
- Fix: Cache DTOs/arrays or minimal attributes you actually need.
- Storing gigantic list payloads
- Fix: Use index + item pattern; compress if necessary (serialize arrays, not models).
- Key collisions from poor naming
- Fix: Include variant, inputs, and a stable separator; consider hashing long filters.
- Ignoring driver behavior
- Fix: Understand Redis/Memcached eviction; set memory alerts; use persistence where needed.
11) End-to-end examples
Example A: User profile with edit invalidation
Goal: Fast profile reads, instant consistency after edits.
Read:
function getUserProfile(int $id): array {
$key = "users:profile:{$id}";
return Cache::remember($key, now()->addMinutes(20), function () use ($id) {
$user = User::with('roles:id,name')->findOrFail($id);
return [
'id' => $user->id,
'name' => $user->name,
'roles' => $user->roles->pluck('name')->all(),
];
});
}
Write (controller or service):
$user->fill($request->validated())->save();
Cache::forget("users:profile:{$user->id}");
Rationale: TTL grants bounded staleness; forget
provides fast convergence.
Example B: Settings cached “forever,” flushed on admin save
Read:
$settings = Cache::rememberForever('settings:all', function () {
return Setting::all()->pluck('value', 'key')->toArray();
});
On admin save:
Cache::forget('settings:all');
Rationale: Data rarely changes; deterministic invalidation; minimal recomputation.
Example C: List + item with tags for selective flushes
Read list (by filters):
$filters = request()->only(['category', 'author', 'q']);
$signature = md5(json_encode($filters));
$page = (int) request('page', 1);
$ids = Cache::tags(['posts', "list:{$signature}"])
->remember("posts:list:{$signature}:p{$page}", 600, function () use ($filters) {
return Post::published()
->when($filters['category'] ?? null, fn ($q, $c) => $q->whereCategory($c))
->when($filters['author'] ?? null, fn ($q, $a) => $q->whereAuthor($a))
->when($filters['q'] ?? null, fn ($q, $qstr) => $q->search($qstr))
->paginate(20)
->pluck('id')
->all();
});
$posts = collect($ids)->map(fn ($id) =>
Cache::tags(['posts', "post:{$id}"])
->remember("posts:item:{$id}", 600, fn () => Post::find($id))
);
On post update:
$post->save();
// Refresh item
Cache::tags(["post:{$post->id}"])->flush(); // or Cache::forget("posts:item:{$post->id}")
// Optionally, flush lists likely affected by this change
// (if you track reverse indexes, use them; otherwise use a broader tag)
Cache::tags(['posts'])->flush(); // careful: this is broad
Rationale: Tag granularity lets you target either one post or all lists.
Example D: Versioned keys for hot dashboards
Read:
$nsKey = 'dash:team:42:ver';
$ver = Cache::rememberForever($nsKey, fn () => 1);
$key = "dash:team:42:v{$ver}";
$data = Cache::remember($key, now()->addMinutes(5), function () {
return buildDashboardWidgets();
});
Invalidate after data import:
Cache::increment('dash:team:42:ver'); // readers switch keys instantly
Rationale: No stampede on delete; graceful rollover.
12) FAQs
Q1) Is rememberForever
safe in production?
Yes—if you have a deterministic invalidation hook. Without one, you’ll serve stale data indefinitely. Use observers, admin actions, or deployment scripts to forget()
or flush()
relevant keys or tags.
Q2) Can I mix remember
and tags()
?
Absolutely. Tags are orthogonal to TTL. Use tags for grouped invalidation and set a TTL to limit staleness if a flush is missed.
Q3) What do I do when tags aren’t supported (file/database drivers)?
Fallback patterns: cache-aside with remember($ttl)
, versioned keys, and event-driven invalidation. Consider moving to Redis for tags and atomic ops.
Q4) How do I prevent cache stampede in Laravel?
Use locks around expensive builders, versioned keys for invalidation, early refresh (serve stale briefly while rebuilding), and jittered TTLs.
Q5) Should I cache Eloquent models or arrays?
Prefer arrays/DTOs to avoid serializing heavy relation graphs and to make payloads stable across code changes.
Q6) What TTL should I pick?
Start with the user tolerance for staleness. If users notice within minutes, keep TTL short (5–15 min) and add event-driven invalidation. If the data is reference-like, push TTL longer or use rememberForever
.
Final takeaways
- Use
remember
when data changes unpredictably or when you lack a perfect invalidation story. - Use
rememberForever
only when you have a clear, automated invalidation point. - Combine well-designed keys, granular tags, versioned namespaces, and locks to control staleness and avoid stampedes.
- Measure cache hit/miss rates and rebuild latencies so you can tune TTLs with confidence.
With these techniques, your Laravel cache will be boring—and boring is exactly what you want in production.
Comments