Performance Optimization

Mastering Caching and Response Optimization in Laravel 12

Learn advanced caching strategies in Laravel 12. Master Redis, response caching, HTTP caching, and optimization techniques to deliver lightning-fast applications.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
03-Nov-2025
11 min read
Mastering Caching and Response Optimization in Laravel 12

Introduction

Caching is the single most effective technique for improving application performance. By storing frequently accessed data in fast-access storage layers, you can reduce database queries, minimize computation, and deliver responses in milliseconds instead of seconds.

A well-implemented caching strategy can reduce server load by 80%, handle 10x more traffic, and dramatically improve user experience. However, improper caching can lead to stale data, memory issues, and difficult debugging.

In this comprehensive guide, we'll explore advanced caching strategies for Laravel 12, covering cache drivers, multi-layer caching, response optimization, HTTP caching, and cache invalidation patterns. You'll learn how to implement production-ready caching that balances performance with data freshness.


Understanding Laravel Cache Layers

Cache Layer Hierarchy

Laravel supports multiple cache layers:

  1. OPcache – PHP bytecode caching (configuration level)
  2. Application Cache – Redis, Memcached, Database (data level)
  3. HTTP Cache – Response caching, CDN (delivery level)
  4. Browser Cache – Client-side storage (client level)

Available Cache Drivers

Laravel 12 supports multiple drivers:

  • Redis – Recommended for production (fast, feature-rich)
  • Memcached – Fast in-memory storage
  • Database – Persistent, slower than memory
  • File – Simple, suitable for development
  • Array – In-memory, request lifecycle only
  • DynamoDB – AWS managed cache

Configuring Redis Cache

1. Install and Configure Redis

Install Redis and PHP extension:

# Install Redis
sudo apt-get install redis-server

# Install PHP Redis extension
composer require predis/predis

Configure in .env:

CACHE_DRIVER=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

2. Configure Multiple Redis Connections

Separate cache and sessions in config/database.php:

'redis' => [
    'client' => env('REDIS_CLIENT', 'predis'),
    
    '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'),
    ],
    
    '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'),
    ],
],

Basic Caching Operations

1. Storing Data in Cache

use Illuminate\Support\Facades\Cache;

// Store for specific duration (seconds)
Cache::put('key', 'value', 3600);

// Store for specific duration (Carbon)
Cache::put('key', 'value', now()->addMinutes(60));

// Store permanently
Cache::forever('key', 'value');

// Store if doesn't exist
Cache::add('key', 'value', 3600);

// Store and retrieve
$value = Cache::remember('users', 3600, function () {
    return DB::table('users')->get();
});

// Store permanently or retrieve
$value = Cache::rememberForever('settings', function () {
    return DB::table('settings')->get();
});

2. Retrieving Cached Data

// Get value
$value = Cache::get('key');

// Get with default
$value = Cache::get('key', 'default');

// Get with closure default
$value = Cache::get('key', function () {
    return DB::table('users')->get();
});

// Check if exists
if (Cache::has('key')) {
    $value = Cache::get('key');
}

// Get and delete
$value = Cache::pull('key');

3. Removing Cached Data

// Delete specific key
Cache::forget('key');

// Delete multiple keys
Cache::forget(['key1', 'key2', 'key3']);

// Clear entire cache
Cache::flush();

// Using array access
unset(Cache::get('key'));

Advanced Caching Techniques

1. Cache Tags (Redis/Memcached Only)

Group related cache items:

// Store with tags
Cache::tags(['people', 'artists'])->put('John', $john, 3600);
Cache::tags(['people', 'authors'])->put('Anne', $anne, 3600);

// Retrieve tagged items
$john = Cache::tags(['people', 'artists'])->get('John');

// Remember with tags
$posts = Cache::tags(['posts', 'user:' . $userId])
    ->remember('user_posts:' . $userId, 3600, function () use ($userId) {
        return Post::where('user_id', $userId)->get();
    });

// Flush all items with a tag
Cache::tags(['people'])->flush(); // Removes both John and Anne
Cache::tags(['artists'])->flush(); // Removes only John

2. Atomic Locks for Cache Stampede Prevention

Prevent multiple processes from regenerating the same cache:

use Illuminate\Support\Facades\Cache;

$value = Cache::lock('cache:posts')->get(function () {
    return Cache::remember('posts', 3600, function () {
        // This expensive operation runs only once
        return Post::with('author', 'tags')->get();
    });
});

// With timeout
$lock = Cache::lock('process:import', 120);

if ($lock->get()) {
    try {
        // Process runs exclusively
        $this->importData();
    } finally {
        $lock->release();
    }
}

// Block and wait
$lock = Cache::lock('process:import', 120);

$lock->block(5, function () {
    // Waits up to 5 seconds for lock
    $this->importData();
});

3. Cache Key Strategies

Design effective cache keys:

// ❌ Poor key structure
Cache::remember('posts', 3600, function () {
    return Post::all();
});

// ✅ Descriptive, namespaced keys
Cache::remember('posts:all:published', 3600, function () {
    return Post::where('status', 'published')->get();
});

// ✅ Include parameters in key
public function getUserPosts($userId, $status = 'published')
{
    $cacheKey = "user:{$userId}:posts:status:{$status}";
    
    return Cache::remember($cacheKey, 3600, function () use ($userId, $status) {
        return Post::where('user_id', $userId)
            ->where('status', $status)
            ->get();
    });
}

// ✅ Use model cache keys
class Post extends Model
{
    public function getCacheKey()
    {
        return sprintf(
            '%s/%s-%s',
            $this->getTable(),
            $this->getKey(),
            $this->updated_at->timestamp
        );
    }
}

Query Result Caching

1. Automatic Query Caching

Cache Eloquent queries efficiently:

// Cache query results
$users = Cache::remember('users:active', 3600, function () {
    return User::where('status', 'active')
        ->with('profile')
        ->get();
});

// Cache with query builder
$posts = Cache::remember('posts:popular', 3600, function () {
    return DB::table('posts')
        ->where('views', '>', 1000)
        ->orderBy('views', 'desc')
        ->limit(10)
        ->get();
});

// Cache aggregate queries
$totalRevenue = Cache::remember('stats:revenue:today', 600, function () {
    return Order::whereDate('created_at', today())
        ->sum('total_amount');
});

2. Model-Level Caching

Implement caching in models:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;

class Post extends Model
{
    protected static function booted()
    {
        // Clear cache on update
        static::updated(function ($post) {
            Cache::tags(['posts'])->flush();
            Cache::forget("post:{$post->id}");
        });
        
        // Clear cache on delete
        static::deleted(function ($post) {
            Cache::tags(['posts'])->flush();
            Cache::forget("post:{$post->id}");
        });
    }
    
    public static function findCached($id)
    {
        return Cache::remember("post:{$id}", 3600, function () use ($id) {
            return static::with(['author', 'tags', 'comments'])->findOrFail($id);
        });
    }
    
    public static function getAllCached()
    {
        return Cache::tags(['posts'])->remember('posts:all', 3600, function () {
            return static::with('author')->published()->get();
        });
    }
}

3. Using Remember Method

Simplify caching with the remember method:

// Basic usage
$value = Cache::remember('key', 3600, function () {
    return expensiveOperation();
});

// With model relationships
$user = User::with(['posts' => function ($query) {
    $query->remember(3600);
}])->find(1);

// Using Rememberable trait (custom implementation)
trait Rememberable
{
    public function scopeRemember($query, $seconds = 3600)
    {
        $key = $this->getCacheKey($query);
        
        return Cache::remember($key, $seconds, function () use ($query) {
            return $query->get();
        });
    }
    
    protected function getCacheKey($query)
    {
        return md5(
            $query->toSql() . serialize($query->getBindings())
        );
    }
}

Response Caching

1. HTTP Response Caching

Cache entire HTTP responses:

// Install spatie/laravel-responsecache
composer require spatie/laravel-responsecache

// Publish config
php artisan vendor:publish --provider="Spatie\ResponseCache\ResponseCacheServiceProvider"

Configure in config/responsecache.php:

return [
    'enabled' => env('RESPONSE_CACHE_ENABLED', true),
    
    'cache_lifetime_in_seconds' => env('RESPONSE_CACHE_LIFETIME', 60 * 60 * 24 * 7),
    
    'cache_store' => env('RESPONSE_CACHE_DRIVER', 'redis'),
];

Use in routes:

use Spatie\ResponseCache\Middlewares\CacheResponse;

Route::get('/posts', [PostController::class, 'index'])
    ->middleware(CacheResponse::class);

// With custom TTL
Route::get('/posts/{post}', [PostController::class, 'show'])
    ->middleware('cache.response:3600');

Clear response cache:

use Spatie\ResponseCache\Facades\ResponseCache;

// Clear all
ResponseCache::clear();

// Clear specific URLs
ResponseCache::forget('/posts');
ResponseCache::forget('/posts/1');

2. Cache Control Headers

Set cache headers manually:

public function show(Post $post)
{
    return response()
        ->view('posts.show', compact('post'))
        ->header('Cache-Control', 'public, max-age=3600')
        ->header('Expires', now()->addHour()->toRfc7231String());
}

// ETags for conditional requests
public function show(Post $post)
{
    $etag = md5($post->updated_at->timestamp);
    
    if (request()->header('If-None-Match') === $etag) {
        return response()->noContent(304);
    }
    
    return response()
        ->view('posts.show', compact('post'))
        ->header('ETag', $etag)
        ->header('Cache-Control', 'public, max-age=3600');
}

3. Middleware-Based Caching

Create custom cache middleware:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class CacheResponse
{
    public function handle(Request $request, Closure $next, $minutes = 60)
    {
        if ($request->method() !== 'GET') {
            return $next($request);
        }
        
        $key = 'route:' . $request->fullUrl();
        
        return Cache::remember($key, $minutes * 60, function () use ($next, $request) {
            return $next($request);
        });
    }
}

View and Fragment Caching

1. Blade Fragment Caching

Cache expensive view fragments:

{{-- Cache for 1 hour --}}
@cache('sidebar', 3600)
    <div class="sidebar">
        <h3>Popular Posts</h3>
        <ul>
            @foreach(Post::popular()->limit(5)->get() as $post)
                <li>{{ $post->title }}</li>
            @endforeach
        </ul>
    </div>
@endcache

{{-- Cache with tags --}}
@cache('user-dashboard', 3600, ['users', 'dashboard'])
    <div class="dashboard">
        <!-- Expensive dashboard content -->
    </div>
@endcache

Create custom Blade directive in AppServiceProvider:

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;

public function boot()
{
    Blade::directive('cache', function ($expression) {
        return "<?php if(! app('cache.store')->has({$expression})) : ?>";
    });
    
    Blade::directive('endcache', function () {
        return "<?php endif; ?>";
    });
}

2. View Compilation Caching

Cache compiled Blade views:

# Cache views in production
php artisan view:cache

# Clear view cache
php artisan view:clear

Configuration and Route Caching

1. Configuration Caching

Cache all configuration files:

# Cache config
php artisan config:cache

# Clear config cache
php artisan config:clear

Important: Environment variables are not accessible when config is cached. Always use config() helper:

// ❌ Don't use env() directly
$apiKey = env('API_KEY');

// ✅ Use config helper
$apiKey = config('services.api.key');

2. Route Caching

Cache route definitions:

# Cache routes
php artisan route:cache

# Clear route cache
php artisan route:clear

Note: Route caching doesn't work with closure-based routes:

// ❌ Won't work with route cache
Route::get('/dashboard', function () {
    return view('dashboard');
});

// ✅ Use controllers
Route::get('/dashboard', [DashboardController::class, 'index']);

Cache Invalidation Strategies

1. Time-Based Invalidation

Simple TTL-based expiration:

// Short TTL for frequently changing data
Cache::put('active_users', $users, 60); // 1 minute

// Medium TTL for moderate changes
Cache::put('popular_posts', $posts, 3600); // 1 hour

// Long TTL for static data
Cache::put('site_settings', $settings, 86400); // 1 day

2. Event-Based Invalidation

Clear cache on model events:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;

class Post extends Model
{
    protected static function booted()
    {
        $clearCache = function () {
            Cache::tags(['posts'])->flush();
            Cache::forget('posts:count');
            Cache::forget('posts:popular');
        };
        
        static::created($clearCache);
        static::updated($clearCache);
        static::deleted($clearCache);
    }
}

3. Manual Cache Busting

Clear specific caches programmatically:

// In controller after update
public function update(Request $request, Post $post)
{
    $post->update($request->validated());
    
    // Clear related caches
    Cache::forget("post:{$post->id}");
    Cache::tags(['posts', 'user:' . $post->user_id])->flush();
    ResponseCache::forget("/posts/{$post->slug}");
    
    return redirect()->route('posts.show', $post);
}

4. Cache Versioning

Use version keys for instant invalidation:

// Get current cache version
$version = Cache::remember('posts:version', 86400, function () {
    return time();
});

// Use version in cache key
$posts = Cache::remember("posts:all:v{$version}", 3600, function () {
    return Post::all();
});

// Invalidate all versioned caches
public function clearPostsCache()
{
    Cache::forget('posts:version');
    // Next request will use new version, effectively clearing cache
}

CDN and Browser Caching

1. Asset Versioning

Use Laravel Mix/Vite for asset versioning:

// vite.config.js
export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
    build: {
        rollupOptions: {
            output: {
                entryFileNames: 'js/[name].[hash].js',
                chunkFileNames: 'js/[name].[hash].js',
                assetFileNames: 'assets/[name].[hash].[ext]',
            },
        },
    },
});

2. Static Asset Caching

Configure in Nginx:

location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

3. CDN Integration

Configure asset URL:

ASSET_URL=https://cdn.yourdomain.com

In config/app.php:

'asset_url' => env('ASSET_URL'),

Monitoring Cache Performance

1. Cache Hit Rate

Monitor cache effectiveness:

// Custom middleware
public function handle($request, Closure $next)
{
    $key = 'cache_stats';
    $stats = Cache::get($key, ['hits' => 0, 'misses' => 0]);
    
    if (Cache::has($request->fullUrl())) {
        $stats['hits']++;
    } else {
        $stats['misses']++;
    }
    
    Cache::put($key, $stats, 3600);
    
    return $next($request);
}

2. Redis Monitoring

Monitor Redis performance:

# Connect to Redis
redis-cli

# Monitor cache statistics
INFO stats

# Watch commands in real-time
MONITOR

# Check memory usage
INFO memory

Conclusion

Mastering caching in Laravel 12 is essential for building high-performance applications. By implementing multi-layer caching strategies, using Redis effectively, optimizing responses, and managing cache invalidation properly, you can dramatically improve application speed and scalability.

Key takeaways:

  • Use Redis for production caching
  • Implement cache tags for organized invalidation
  • Cache query results with proper TTL
  • Use response caching for static content
  • Implement proper cache invalidation strategies
  • Monitor cache hit rates and optimize
  • Configure browser and CDN caching

Need help implementing advanced caching strategies? NeedLaravelSite specializes in performance optimization and caching architecture. Contact us for expert Laravel development services.


Related Resources:


Article Tags

Laravel caching Laravel 12 cache Laravel Redis caching Laravel response cache Laravel HTTP caching Laravel cache optimization Laravel cache tags Laravel query cache Laravel view cache Laravel route cache Laravel cache strategies Response time optimization Laravel Laravel CDN caching Laravel API caching

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products