← Back to Blog
RedisLaravelPHPCaching

Redis Caching Patterns for Laravel APIs: Beyond Cache::remember()

Why Cache::remember() Isn't Enough

Cache::remember() is great for simple cases. Production cache architecture requires thinking about key namespacing, TTL strategy, stampede protection, and invalidation. Getting any of these wrong under load can cause more damage than having no cache at all.

Key Namespacing Strategy

Flat cache keys are a ticking time bomb. They collide across deployments, tenants, and feature versions.

// Bad: flat, unversioned, will collide
Cache::remember('users', 3600, fn() => User::all());

// Good: versioned, tenant-scoped, descriptive
$key = "v2:tenant:{$tenantId}:users:active:page:{$page}";
Cache::remember($key, 3600, fn() => ...);

Add a version prefix. When your data model changes, bump the version — old keys expire naturally without a manual flush.

Cache Stampede Protection

When a cached item expires and 1,000 concurrent requests try to regenerate it simultaneously, your database dies. Laravel's atomic locks solve this cleanly:

$data = Cache::get($key);
if (!$data) {
    $lock = Cache::lock("lock:{$key}", 10);
    try {
        if ($lock->get()) {
            $data = expensiveQuery();
            Cache::put($key, $data, 3600);
        } else {
            $lock->block(5); // wait up to 5s for the other process
            $data = Cache::get($key);
        }
    } finally {
        optional($lock)->release();
    }
}

Redis Hash for Partial Updates

Instead of caching a full user object as a serialized blob (requiring a full rewrite on any change), use Redis hashes:

Redis::hmset("user:{$id}", [
    'name'  => $user->name,
    'email' => $user->email,
    'plan'  => $user->plan,
]);
Redis::expire("user:{$id}", 3600);

// Update just the plan without touching the rest of the cached data
Redis::hset("user:{$id}", 'plan', 'enterprise');

This pattern eliminates the cache invalidation problem for objects with multiple independent fields.

Sorted Sets for Leaderboards and Feeds

Redis sorted sets (ZADD, ZRANGE) are ideal for ranked data:

// Increment a user's score
Redis::zadd('leaderboard:monthly', ['increment' => true], 10, "user:{$userId}");

// Fetch top 10
$top10 = Redis::zrevrange('leaderboard:monthly', 0, 9, 'WITHSCORES');

No SQL aggregate query. No locks. O(log N) writes, O(log N + M) reads.

Cache Invalidation Strategies

The two practical approaches:

Tag-based invalidation (Laravel's built-in support):

Cache::tags(['posts', "user:{$userId}"])->remember($key, 3600, fn() => ...);

// Later, flush everything tagged 'posts'
Cache::tags(['posts'])->flush();

Event-driven invalidation: Listen for model events and explicitly delete affected keys. More granular, more code, but no risk of tag-based flush wiping unrelated data.

Choose tags for simplicity, events for precision.