Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Redis Caching: Speed Up Your Laravel/PHP App

By Kokil Thapa | Last reviewed: August 2026

Slow database queries and repetitive computation are the most common bottlenecks I diagnose when auditing PHP applications. Implementing Redis caching: Speed Up Your Laravel/PHP App is rarely about installing a package; it requires understanding data volatility, serialization costs, and invalidation logic. Whether you are running a high-traffic eCommerce store or a legal-tech portal, moving from file-based caching to an in-memory data store like Redis is often the single highest-ROI infrastructure change you can make.

Before diving into configuration, understand that caching is an architectural decision, not just a performance tweak. If your application serves dynamic content where freshness matters—like a Laravel developer in Nepal building real-time booking systems—you must balance speed against data consistency. For a deeper look at how this fits into broader API design, see my notes on Laravel API best practices.

How do you configure Redis as the primary cache driver in Laravel 12?

Laravel 12 (requiring PHP 8.2 minimum) ships with first-class Redis support, but the default configuration assumes a local development setup. In production, you need to tune connection persistence, serialization, and prefixing to avoid collisions and memory leaks.

Choosing between PhpRedis and Predis

You have two client options. PhpRedis is a C-extension that offers superior performance and lower memory overhead because it runs natively. Predis is a pure-PHP library that is easier to install (just Composer) but slower under heavy load. On every production server I manage, I install the PhpRedis extension via PECL or the OS package manager.

# Install PhpRedis on Ubuntu 24.04
sudo apt-get install php8.4-redis

# Verify installation
php -m | grep redis

# Or via PECL if package not available
sudo pecl install redis
echo "extension=redis.so" | sudo tee /etc/php/8.4/mods-available/redis.ini
sudo phpenmod redis

Configuring database separation

A critical mistake is using Redis database 0 for everything: cache, sessions, queues, and Pub/Sub. When you run php artisan cache:clear, you risk wiping active user sessions or killing queued jobs. Always separate concerns by assigning distinct database indices in config/database.php.

// config/database.php
'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),

    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
        'persistent' => true, // Reuse connections across requests
    ],

    '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'), // General app data
    ],

    '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'), // ISOLATED cache store
    ],

    'session' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_SESSION_DB', '2'), // ISOLATED sessions
    ],
],
Redis Database Separation StrategyLaravel AppPHP 8.4 + FPMDB 0: QueuesJobs & WorkersDB 1: CacheApp & Query CacheDB 2: SessionsUser State⚠️ Never Mix Databasescache:clear on DB 0kills active jobsand user sessions
Separating Redis databases prevents accidental data loss during maintenance operations

What are the most effective caching patterns for Eloquent queries?

Caching entire model instances is often wasteful. Serialized Eloquent models carry relationship metadata, hidden attributes, and appended accessors that bloat memory. Instead, cache only the data shape your view or API response actually needs. This reduces serialization overhead and makes invalidation more predictable.

The Repository Pattern with Tagged Caching

Tags are essential for group invalidation. Without them, you cannot clear "all posts for category X" without iterating through every possible key. Redis supports tags natively, unlike Memcached. Always wrap cached queries in a service or repository layer rather than scattering Cache::remember() calls inside controllers.

class ProductRepository
{
    public function getActiveProducts(int $categoryId): Collection
    {
        $cacheKey = "products:category:{$categoryId}:active";

        return Cache::tags(['products', "category:{$categoryId}"])
            ->remember($cacheKey, now()->addHours(6), function () use ($categoryId) {
                // Select only required columns to reduce payload size
                return Product::where('category_id', $categoryId)
                    ->where('is_active', true)
                    ->select(['id', 'name', 'price', 'slug', 'thumbnail'])
                    ->orderByDesc('updated_at')
                    ->limit(50)
                    ->get();
            });
    }

    public function invalidateCategory(int $categoryId): void
    {
        // Clears ALL cached items tagged with this specific category
        Cache::tags(["category:{$categoryId}"])->flush();
    }
}

Avoiding the Thundering Herd with Atomic Locks

When a popular cache key expires, hundreds of concurrent requests may simultaneously hit the database to regenerate it. This "thundering herd" effect can crash your MySQL instance. Use atomic locks to ensure only one process regenerates the value while others wait or receive stale data.

$value = Cache::lock('products:regenerate', 10)->block(5, function () {
    // Only ONE process enters here at a time
    return $this->expensiveQuery();
});

// Alternative: Return stale data immediately, regenerate in background
if (Cache::has($key)) {
    return Cache::get($key);
}

if (Cache::lock("lock:{$key}", 30)->get()) {
    try {
        $fresh = $this->expensiveQuery();
        Cache::put($key, $fresh, now()->addHours(6));
        return $fresh;
    } finally {
        Cache::lock("lock:{$key}")->release();
    }
}

// Other workers return null or fallback while lock holder regenerates
return null;
Atomic Lock: Preventing Thundering HerdRequest ARequest BRequest CRedis LockAcquired ✓Blocked ✗Blocked ✗MySQLSingle QueryRedis CacheWrite ResultGets LockWaits / Stale
Atomic locks ensure only one request regenerates expired cache entries

How does Redis compare to file and database caching for PHP applications?

Understanding trade-offs prevents over-engineering. File caching works fine for low-traffic brochure sites. Database caching adds latency. Redis shines when read volume is high, data changes infrequently relative to reads, or you need sub-millisecond response times for session/API workloads.

CriteriaFile CacheDatabase CacheRedis Cache
Read Latency5–50ms (disk I/O)10–100ms (query + network)<1ms (in-memory)
ConcurrencyPoor (file locks)Moderate (row locks)Excellent (non-blocking)
Tag SupportNoYes (slow)Yes (native sets)
Atomic OperationsNoLimitedFull (INCR, SETNX, Lua)
Session StorageUnsafe (shared hosting)Slow under loadIdeal (fast, isolated)
Infrastructure CostZeroExisting DB+Rs 1,500–5,000/mo VPS
Best ForStatic config, devLow-traffic appsProduction, APIs, eCommerce

For Nepal-based projects where budget sensitivity is real, I often start clients on file caching and migrate to Redis only when monitoring shows cache misses causing measurable latency. The jump from file to Redis typically yields 10–50x throughput improvement for read-heavy endpoints, but the operational complexity (monitoring, backups, security) must be justified by actual traffic.

What are the common cache invalidation mistakes in production Laravel apps?

Invalidation bugs cause more production incidents than any other caching issue. Stale data erodes user trust faster than slow pages. These patterns recur across projects I've audited, from legal portals to eCommerce platforms.

Mistake 1: Missing Model Observer Hooks

If you cache product listings but forget to flush on update/delete, users see phantom inventory. Bind cache invalidation directly to model lifecycle events, never rely on manual controller calls.

class ProductObserver
{
    public function saved(Product $product): void
    {
        Cache::tags(['products', "category:{$product->category_id}"])->flush();
        Cache::tags(['products', "product:{$product->id}"])->flush();
    }

    public function deleted(Product $product): void
    {
        $this->saved($product); // Same invalidation logic
    }
}

Mistake 2: Over-Caching Mutable Data

User dashboards, cart contents, and admin panels change per-request. Caching these creates staleness bugs that are nearly impossible to debug. Reserve Redis for shared, slowly-changing reference data: navigation menus, category trees, pricing tiers, feature flags, and aggregated statistics.

Mistake 3: Ignoring Serialization Format Changes

When you add a new attribute to an Eloquent model or change an accessor, existing cached serialized objects become corrupt or incomplete. After deployments that modify model structure, always run php artisan cache:clear or version your cache keys with a deployment hash.

// Versioned cache key survives schema changes
$version = config('app.cache_version', 'v1');
$key = "{$version}:products:category:{$categoryId}";

// In deploy script or CI pipeline:
// php artisan config:cache
// php artisan cache:clear  # Safe because cache DB is isolated
Should You Cache This Data?Data Access Pattern?High Read / Low WriteFrequent UpdatesShared Across Users?User-Specific / Mutable?✅ CACHECategories, Menus⚠️ SHORT TTLSearch Results❌ NO CACHECart, Dashboard

Frequently Asked Questions

Redis is an in-memory key-value store used as a high-speed cache driver for Laravel applications.

Self-hosted on a VPS costs Rs 1,500–3,000 monthly (~USD 11–22), while managed services range USD 15–30 monthly.

Use Redis when cache read/write frequency exceeds 100 operations per second or sub-millisecond latency is required.

Set CACHE_STORE=redis in your .env file and ensure the redis connection is defined in config/database.php. Laravel 12 uses this environment variable instead of the deprecated CACHE_DRIVER. Verify connectivity by running php artisan tinker and executing Cache::put('test', true, 60) followed by Cache::get('test'). On Ubuntu servers, confirm the redis-server service is active via systemctl status redis-server before testing application connectivity.

In my experience with production Laravel applications, Redis generally outperforms Memcached for session storage because it supports data persistence, complex data types, and atomic operations. Memcached may show marginally better raw throughput for simple key-value gets, but Redis offers superior reliability during restarts and supports tagged caching natively through Laravel's cache facade. For legal-tech portals handling sensitive client sessions, Redis persistence prevents users from being logged out after server maintenance windows.

Install the php-redis extension via PECL or your package manager. On Ubuntu 24.04 with PHP 8.4, run sudo apt install php8.4-redis then restart PHP-FPM with sudo systemctl restart php8.4-fpm. Verify installation by checking phpinfo() output or running php -m | grep redis. The Predis pure-PHP library works without extensions but adds 2-3ms overhead per operation, which compounds under load. Always prefer the C extension for production environments serving real traffic.

Use Laravel's Cache::lock() method to implement mutex locks around expensive cache regeneration. When multiple requests hit an expired cache key simultaneously, only one process regenerates the value while others wait or return stale data. On a travel booking platform I built, this prevented database overload when popular trekking itinerary caches expired during peak browsing hours. Combine with Cache::rememberForever() for critical reference data and use probabilistic early expiration to spread regeneration across time rather than clustering at exact TTL boundaries.

Yes, Redis serves as both cache store and queue backend in Laravel. Configure separate databases by setting REDIS_CACHE_DB=0 and REDIS_QUEUE_DB=1 in your .env to isolate workloads. This prevents queue workers consuming memory needed for hot cache keys. On eCommerce projects like Nepal Gift Card, I run sessions on database 0, cache on database 1, and queues on database 2. Monitor each database independently using redis-cli INFO keyspace to detect memory pressure before it causes cache evictions or failed jobs.

With default volatile-lru eviction policy, all cached data persists across restarts if RDB snapshots or AOF logging are enabled. Without persistence configured, the entire cache clears on restart, causing immediate database load spikes. In production deployments on Ubuntu servers, I configure appendonly yes in redis.conf for AOF persistence with auto-aof-rewrite-percentage 100. For non-critical cache layers like rendered HTML fragments, accepting data loss on restart reduces disk I/O. Always test failure scenarios during staging validation before deploying persistence configuration changes to live environments.

Run redis-cli INFO memory to check used_memory_human and mem_fragmentation_ratio values. Set maxmemory in redis.conf to prevent unbounded growth, typically 70% of available RAM on dedicated instances. Configure maxmemory-policy allkeys-lru for cache-only instances so Redis evicts least-recently-used keys automatically. On shared EC2 infrastructure hosting multiple sister sites, I set up Prometheus redis_exporter with Grafana dashboards tracking memory trends over 30 days. Alert when fragmentation ratio exceeds 1.5 or used memory approaches 85% of maxmemory threshold.

Cache tags require the php-redis extension; Predis does not support them efficiently. Verify your connection uses the phpredis driver in config/database.php by checking 'driver' => 'phpredis'. Tags create additional Redis sets for each tag, increasing memory overhead proportionally to tag cardinality. On directory sites with hundreds of category tags, I observed 40% higher memory consumption versus untagged caching. Consider structured key naming conventions like users:{id}:profile instead of tags when cardinality exceeds 50 unique tags to reduce set operations and simplify manual cache invalidation debugging.

Bind Redis to 127.0.0.1 in redis.conf unless remote access is absolutely necessary. Enable requirepass with a strong 32-character minimum password stored in Laravel's .env file. Configure UFW to block port 6379 from external networks with sudo ufw deny 6379. Disable dangerous commands like FLUSHALL and CONFIG via rename-command directives in redis.conf. On legal-tech portals storing sensitive document metadata in cache, I additionally enable TLS encryption between Laravel and Redis when they reside on separate hosts. Never expose Redis directly to the internet without authentication and network-level restrictions.

Common causes include PHP-FPM worker exhaustion, Redis maxclients limit reached, or network latency between application and cache servers. Check Redis logs for rejected connections and slowlog get 10 for blocking commands. After Deployer 7 releases, stale PHP-FPM processes sometimes retain old Redis connections; always run sudo systemctl reload php8.4-fpm post-deploy. On one production system, timeout errors traced to opcache holding cached Redis configuration from previous release; adding opcache_reset() to deploy script resolved intermittent failures. Monitor connection pool metrics alongside application error rates during deployment windows.

Self-hosting on a Kathmandu or Singapore VPS provides lower latency for Nepal users and avoids cross-border payment complications. Redis Cloud offers automated backups and scaling but bills in USD with minimum tiers around USD 30 monthly. For projects billing clients in NPR, self-hosted Redis on a Rs 2,500/month VPS keeps infrastructure costs predictable. I self-host for most Nepal legal-tech and eCommerce projects, reserving managed services for clients requiring 99.9% SLA guarantees or multi-region failover. Factor in your team's operational capacity; managing Redis requires monitoring, backup verification, and upgrade planning that managed services handle automatically.

Create an Artisan command that runs Cache::put() and Cache::get() operations 10,000 times with microtime(true) measurements. Test both drivers using identical payloads representing your actual cached data structures. File cache often wins for small, infrequently-accessed data due to OS page caching, while Redis dominates for high-concurrency workloads exceeding 50 concurrent requests. On a florist eCommerce site, Redis reduced average page render time from 180ms to 45ms during flash sales with 200+ simultaneous users. Always benchmark with production-representative data sizes and concurrency levels rather than synthetic microbenchmarks that mislead architectural decisions.

Share this article

Quick Contact Options
Choose how you want to connect me: