Server-Side Caching

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

By Domain India Team · DomainIndia EngineeringPublished 9 min read
Knowledge base article
Contents (12 sections)

A busy Laravel app lives or dies by its cache. This guide covers the Redis-backed patterns that keep it fast under load, and what to use instead where Redis is not available.

Key takeaways

Laravel's cache layer over Redis gives you tags (invalidate related keys together), atomic locks (stop two workers doing the same expensive job) and stale-while-revalidate caching with Cache::flexible(). This guide covers the patterns that matter in production. Redis is not available on Domain India shared hosting or on the App Platform, so run these patterns on your own VPS, or use Laravel's database cache driver where Redis isn't available.

The cache layer choice

DriverSpeedTags supportBest for
fileSlow, disk I/ONoSmall single-server sites, development
databaseMediumNoSmall Laravel apps where Redis isn't available
memcachedFastYesExisting setups; Redis is the better choice for new apps
redisFastYesProduction Laravel on a server you control

Use Redis when you can run it. Where your Laravel app runs on Domain India decides what you can use:

  • Shared hosting (cPanel, DirectAdmin, Webuzo): no Redis or Memcached server. Use the database or file cache driver; tags and Redis locks are not available there. Composer can't run on shared hosting, so build vendor/ locally and upload it.
  • App Platform: there is no Redis add-on. Each plan includes a PostgreSQL database, so use Laravel's database cache driver on it. Only Node.js is detected automatically, so a Laravel app needs your own Dockerfile.
  • VPS: install Redis (or Valkey) yourself from your OS packages. A VPS is self-managed, so you install, secure and update it.
Where Redis runs at Domain India

Redis is not offered on any shared hosting plan and is not an App Platform add-on. If your app needs Redis for cache, sessions, locks or queues, run it on your own VPS and keep it bound to 127.0.0.1.

Step 1: Configure Laravel for Redis

.env (Laravel 11 and later use CACHE_STORE; older versions use CACHE_DRIVER):

code
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=your-strong-password
REDIS_DB=0
REDIS_CACHE_DB=1     # separate DB for cache vs session

config/database.php: check the Redis client and connections:

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 the phpredis extension on your VPS. On Ubuntu or Debian the package is simplest; PECL works on any Linux:

bash
# Ubuntu / Debian
sudo apt install php-redis

# Any distribution, via PECL
sudo pecl install redis
echo "extension=redis.so" | sudo tee /etc/php.d/50-redis.ini

php -m | grep -i redis

If you can't install an extension, set REDIS_CLIENT=predis and add the predis/predis package to your project.

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());

The Redis driver supports tags. Tag related entries, then invalidate them 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
Tags need Redis or Memcached

Tags don't work with the file, database or DynamoDB drivers. Your production and development environments must use the same driver, or tag code that works locally will fail in production.

Step 4: Atomic locks (prevent race conditions)

Two concurrent requests try to generate the same expensive report. Without a lock, both compute it and the work is wasted.

php
$report = Cache::get('expensive.report');

if ($report === null) {
    $report = Cache::lock('report.lock', 10)->block(5, function () {
        // Another process may have filled the cache while we waited for the lock
        return Cache::remember('expensive.report', 3600, fn () => generateExpensiveReport());
    });
}

block(5, …) waits up to 5 seconds for the lock and throws LockTimeoutException if it can't get it. The lock itself expires after 10 seconds, so a crashed worker can't hold it forever.

Step 5: Cache stampede prevention

The classic problem: a popular key expires at 10:00:00 and 1,000 concurrent requests hit the database at once.

Fix 1: Atomic lock (above). Only one request regenerates; the others wait briefly and then read the fresh value.

Fix 2: Stale-while-revalidate with Cache::flexible() (Laravel 11.23 and later):

php
// Fresh for 5 minutes. Between 5 and 10 minutes old, the stale value is served
// and refreshed in the background after the response is sent.
$stats = Cache::flexible('dashboard.stats', [300, 600], function () {
    return computeDashboardStats();
});

This is the soft-TTL/hard-TTL pattern built in: users almost never wait for a recompute, and only one refresh runs at a time.

Fix 3: Probabilistic early expiration. Each request has a small, growing chance of refreshing the value shortly before it expires, so refreshes spread out instead of all landing at the expiry second. Store the expiry time and the recompute cost next to the value:

php
function rememberEarly(string $key, int $ttl, callable $callback, float $beta = 1.0)
{
    $entry = Cache::get($key); // ['value' => ..., 'expires' => ..., 'delta' => ...]
    $rand  = mt_rand(1, mt_getrandmax()) / mt_getrandmax();

    if ($entry && microtime(true) - $entry['delta'] * $beta * log($rand) < $entry['expires']) {
        return $entry['value'];
    }

    $start = microtime(true);
    $value = $callback();
    $delta = microtime(true) - $start;

    Cache::put($key, [
        'value'   => $value,
        'expires' => microtime(true) + $ttl,
        'delta'   => $delta,
    ], $ttl);

    return $value;
}

For most apps, Cache::flexible() is the simpler choice.

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();
});

Cache the queries you have measured as slow, and invalidate them on write, rather than caching every query automatically.

Session in Redis

Instead of files (the default in older versions) or the database, store sessions in Redis. It is faster and works across several app servers.

code
SESSION_DRIVER=redis
SESSION_CONNECTION=default

Benefits: no sticky sessions needed, and you can wipe all sessions at once after a security incident.

Monitoring cache health

Redis INFO:

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

Laravel Pulse has a cache card that shows hits and misses per key in production.

Laravel Telescope (development only) shows the cache operations of each request.

Laravel Horizon is a dashboard for Redis queues, not for the cache.

Rough targets: a hit rate above 80% for query cache and above 50% for page cache.

Common pitfalls

Cache key too specific
user.42.dashboard.2026-04-24T10:15:42 makes a new key every second. Round timestamps to the minute or hour.
No prefix per environment
Staging cache pollutes production when both share one Redis. REDIS_PREFIX must differ.
Tags use extra keys
Each tag keeps a reference set, so flushing one tag over 10,000 keys can be slow. Keep tag granularity sensible.
Caching authenticated pages by URL
This leaks one user's page to another. Include the user ID in the key, or skip the cache for logged-in users.
Serialising huge models
Cache::put('user', $user) serialises loaded relations too. Cache ->toArray() or a small DTO.
No TTL, then forgotten
With Cache::rememberForever() a bad value never heals itself. Prefer an explicit long TTL (24 hours, 7 days).

FAQ

Can I use Redis with Laravel on Domain India shared hosting?

No. Redis and Memcached are not available on Domain India shared hosting (cPanel, DirectAdmin or Webuzo), and the App Platform has no Redis add-on. Use Laravel's database or file cache driver there, or run Redis yourself on a Domain India VPS.

Redis or Valkey?

Valkey is a Linux Foundation fork of Redis and is compatible with the Redis protocol, so Laravel's redis driver works with it unchanged. Use whichever your operating system ships; AlmaLinux 10 ships Valkey.

How much RAM does Redis need?

It depends on how many keys you store and how big the values are. Start small, check redis-cli INFO memory under real traffic, and set maxmemory with maxmemory-policy allkeys-lru so Redis evicts old keys instead of running out of memory.

Does php artisan cache:clear clear tagged entries?

Yes. It clears the whole cache store, including tagged entries. For a targeted clear, use Cache::tags(['tag1'])->flush().

Should I cache in the controller, the model or the query layer?

Cache at the layer closest to the expensive operation, which is usually the repository or query layer. Controller-level caching is too coarse, because different users can end up sharing one entry, and caching inside model observers is easy to forget about.

Can I share one Redis between several Laravel apps?

Yes, but give each app a different REDIS_PREFIX or different REDIS_DB numbers. Sharing cache between apps couples them, so keep it separate where you can.

Ready to run Redis-powered Laravel? Choose a Domain India VPS and install Redis yourself, or read Server-side caching for what works on shared hosting.

Run Redis on your own server

A VPS gives you root access to install Redis or Valkey and tune it for your Laravel app.

View 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