
August 17, 2026
8 min read
Table of Contents
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.
predis or phpredis extension, set CACHE_DRIVER=redis in your environment, and strategically cache expensive Eloquent queries using taggable keys. Always define explicit TTLs and use atomic locks to prevent race conditions during regeneration.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
],
], 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; 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.
| Criteria | File Cache | Database Cache | Redis Cache |
|---|---|---|---|
| Read Latency | 5–50ms (disk I/O) | 10–100ms (query + network) | <1ms (in-memory) |
| Concurrency | Poor (file locks) | Moderate (row locks) | Excellent (non-blocking) |
| Tag Support | No | Yes (slow) | Yes (native sets) |
| Atomic Operations | No | Limited | Full (INCR, SETNX, Lua) |
| Session Storage | Unsafe (shared hosting) | Slow under load | Ideal (fast, isolated) |
| Infrastructure Cost | Zero | Existing DB | +Rs 1,500–5,000/mo VPS |
| Best For | Static config, dev | Low-traffic apps | Production, 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 
