Server-Side Caching

Laravel + Redis: Cache Tags, Locks, and Stampede Prevention

By Domain India Team · DomainIndia EngineeringPublished · Updated 5 min read
Knowledge base article
Contents (12 sections)
TL;DR

Laravel's cache abstraction over Redis gives you tags (invalidate related keys together), atomic locks (prevent race conditions), and cache-aside patterns. This guide covers the patterns that matter in production: tagged cache, stampede prevention, and fine-grained TTL control on DomainIndia hosting.

The cache layer choice

DriverSpeedTags supportBest for
fileSlow — disk IONoDev only, never prod
databaseMediumNoSmall Laravel apps, no Redis available
memcachedFastPartialLegacy; not recommended for new apps
redisFastYesProduction Laravel

Use Redis. On DomainIndia:

  • Shared cPanel: Redis is available; connection via Unix socket
  • VPS: install locally (sudo dnf install redis) or run as Docker
  • App Platform: add Redis service with one click

Step 1 — Configure Laravel for Redis

.env:

code
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DB=0
REDIS_CACHE_DB=1     # separate DB for cache vs session

config/database.php — verify Redis client:

php
'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),
    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
    ],
    'default' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_DB', 0),
    ],
    'cache' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_CACHE_DB', 1),
    ],
],

Install phpredis via PECL (fastest):

bash
sudo pecl install redis
echo "extension=redis.so" | sudo tee /etc/php.d/50-redis.ini
php -m | grep -i redis

Step 2 — Basic caching patterns

Remember (cache-aside):

php
$products = Cache::remember('products.active', 300, function () {
    return Product::active()->with('category')->get();
});

Invalidate on write:

php
Product::create($data);
Cache::forget('products.active');

Forever (until manually cleared):

php
Cache::rememberForever('settings.all', fn() => Setting::all());

Redis driver supports tags natively. Tag related entries, invalidate all at once.

php
// On write:
Cache::tags(['products', 'category:5'])->put("product.$id", $product, 300);

// Elsewhere:
Cache::tags(['category:5'])->put('category.summary.5', $summary, 300);

// When something in category 5 changes:
Cache::tags(['category:5'])->flush();
// → both product and summary entries cleared
Warning

Tags work only with Redis and Memcached, not file/database drivers. Your production and dev environments must match, or you'll hit surprises.

Step 4 — Atomic locks (prevent race conditions)

Two concurrent requests trying to generate the same expensive report. Without a lock, both compute it — wasted work.

php
$value = Cache::remember('expensive.report', 3600, function () {
    return Cache::lock('report.lock', 10)->block(5, function () {
        // Re-check inside lock — another process may have populated while we waited
        return Cache::get('expensive.report') ?? generateExpensiveReport();
    });
});

Or simpler — use Laravel's built-in stampede protection:

php
// Laravel 10+: atomic rememberWithLock
$value = Cache::rememberWithLock('expensive.report', 3600, function () {
    return generateExpensiveReport();
});

Step 5 — Cache stampede prevention

Classic problem: cache expires at 10:00:00, 1,000 concurrent requests stampede the DB.

Fix 1: Atomic lock (above) — only one regenerates; others wait briefly.

Fix 2: Probabilistic early expiration:

php
public static function rememberWithEarlyExpiration(string $key, int $ttl, callable $callback)
{
    $data = Cache::get($key);
    $ttlRemaining = Cache::ttl($key);

    // 10% chance to regenerate when TTL < 20% remaining
    if ($data && $ttlRemaining > ($ttl * 0.2)) {
        return $data;
    }

    if ($data && rand(0, 100) < 90) {
        return $data;  // serve stale, wait a bit
    }

    $fresh = $callback();
    Cache::put($key, $fresh, $ttl);
    return $fresh;
}

Fix 3: Soft TTL + hard TTL:

php
$data = Cache::remember("$key.soft", 300, function () use ($key, $callback) {
    $fresh = $callback();
    Cache::put("$key.hard", $fresh, 600);  // longer backup TTL
    return $fresh;
});

// On cache miss (hard expired):
return Cache::get("$key.hard") ?? $callback();

Step 6 — Full-page cache (middleware)

For guest-accessible pages:

php
// app/Http/Middleware/PageCache.php
public function handle($request, Closure $next)
{
    if ($request->user() || $request->method() !== 'GET') {
        return $next($request);
    }

    $key = 'page.' . sha1($request->fullUrl());
    $cached = Cache::tags(['page-cache'])->get($key);
    if ($cached) {
        return response($cached['body'], $cached['status'])
            ->withHeaders($cached['headers'])
            ->header('X-Cache', 'HIT');
    }

    $response = $next($request);
    if ($response->isSuccessful()) {
        Cache::tags(['page-cache'])->put($key, [
            'body'    => $response->getContent(),
            'status'  => $response->getStatusCode(),
            'headers' => array_filter($response->headers->all(), fn($k) =>
                !in_array($k, ['set-cookie', 'date']), ARRAY_FILTER_USE_KEY),
        ], 300);
    }
    return $response->header('X-Cache', 'MISS');
}

Invalidate all page cache on content edit:

php
Cache::tags(['page-cache'])->flush();

Step 7 — Query caching

php
$users = Cache::remember('users.active', 120, function () {
    return User::where('is_active', true)->get();
});

Or use laravel/scout + rennokki/laravel-eloquent-query-cache for automatic query-level caching.

Session in Redis

Instead of files (default), store sessions in Redis — faster, and works across multiple app servers.

code
SESSION_DRIVER=redis
SESSION_CONNECTION=default

Benefits: sticky sessions not needed, session count visible in Redis, easier to wipe all sessions (security incident).

Monitoring cache health

Redis INFO:

bash
redis-cli INFO stats
# keyspace_hits, keyspace_misses → compute hit rate

Laravel Horizon (for queue + cache stats) — install via composer require laravel/horizon.

Laravel Telescope (dev-only) — shows cache ops per request.

Target: cache hit rate > 80% for query cache, > 50% for page cache.

Common pitfalls

Cache key too specific
user.42.dashboard.2026-04-24T10:15:42 — every second a new key. Round timestamps to minute or hour.
No prefix per environment
staging cache pollutes prod when sharing Redis. REDIS_PREFIX must differ.
Laravel tags use multiple keys
each Cache::tags([...]) stores a reference set. Invalidating 1 tag on 10K keys can be slow. Keep tag granularity sensible.
Caching authenticated pages by URL
breaks user privacy. Always include user ID in key, or skip cache for authed.
Serialising huge models
Cache::put('user', $user) serialises relations recursively. Use ->toArray() or DTO.
No TTL = forever + forgot
Cache::rememberForever() means a bug never self-heals. Prefer explicit long TTL (24h, 7d).

FAQ

Redis or Valkey?

Valkey is a Redis fork (Linux Foundation). API-compatible; drop-in replacement. Use whichever your OS ships with — AlmaLinux 10+ ships Valkey.

How much RAM for Redis?

Rule: sum of cached data × 1.3 (overhead). For a typical Laravel app: 512 MB Redis handles ~100K keys. Set maxmemory + maxmemory-policy allkeys-lru to cap.

Does php artisan cache:clear clear tags?

Yes — clears all cache entries. For surgical clear, use Cache::tags(['tag1'])->flush().

Cache in controller vs model vs query?

Cache at the layer closest to the expensive operation. Repository / query layer is usually right. Controller-level is too coarse (different users see same cache); model observers are too hidden.

Can I share cache across Laravel apps?

Yes, but use different REDIS_PREFIX or different REDIS_DB numbers. Or avoid — coupling apps via cache is fragile.

Redis-powered Laravel runs cleanly on a DomainIndia VPS. Explore VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Laravel + Redis Cache — Tags, Locks, Stampede Prevention