Laravel Framework

Caching Strategies

15 min Lesson 20 of 45

Caching Strategies in Laravel

Caching is one of the most effective ways to improve application performance. Laravel provides a unified API for various caching backends, making it easy to store and retrieve data quickly, reducing database queries and expensive computations.

Cache Drivers

Configure your cache driver in .env:

# File cache (default, good for development) CACHE_DRIVER=file # Array cache (stored in memory, only lasts for request) CACHE_DRIVER=array # Database cache CACHE_DRIVER=database # Redis cache (recommended for production) CACHE_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 # Memcached cache CACHE_DRIVER=memcached MEMCACHED_HOST=127.0.0.1 MEMCACHED_PORT=11211 # DynamoDB cache CACHE_DRIVER=dynamodb AWS_ACCESS_KEY_ID=your-key AWS_SECRET_ACCESS_KEY=your-secret

For database cache, create the cache table:

php artisan cache:table php artisan migrate
Note: Redis is recommended for production due to its speed, persistence options, and advanced features. Install predis: composer require predis/predis

Storing and Retrieving Data

Basic cache operations:

use Illuminate\Support\Facades\Cache; // Store data (forever) Cache::put('key', 'value'); // Store with expiration (seconds) Cache::put('key', 'value', 600); // 10 minutes // Store with Carbon instance Cache::put('key', 'value', now()->addMinutes(10)); // Store if key doesn't exist Cache::add('key', 'value', 600); // Store forever Cache::forever('key', 'value'); // Retrieve data $value = Cache::get('key'); // Retrieve with default value $value = Cache::get('key', 'default'); // Retrieve with closure default $value = Cache::get('key', function () { return DB::table('users')->get(); }); // Retrieve and delete $value = Cache::pull('key'); // Check if key exists if (Cache::has('key')) { // } // Check if key doesn't exist if (Cache::missing('key')) { // }

Cache Remember Pattern

The most common caching pattern - retrieve or compute and store:

// Remember for specified time $users = Cache::remember('users', 600, function () { return DB::table('users')->get(); }); // Remember forever $settings = Cache::rememberForever('settings', function () { return DB::table('settings')->pluck('value', 'key'); }); // Forget (delete) cache Cache::forget('key'); // Clear all cache Cache::flush(); // Increment/Decrement Cache::increment('visits'); Cache::increment('visits', 5); Cache::decrement('credits'); Cache::decrement('credits', 3); // Retrieve and store (atomic) $value = Cache::remember('key', 600, function () { return expensiveOperation(); });

Cache Tags

Group related cache items for easier management (Redis and Memcached only):

// Store with tags Cache::tags(['users', 'admins'])->put('admin:1', $admin, 600); Cache::tags(['users'])->put('user:1', $user, 600); // Retrieve with tags $admin = Cache::tags(['users', 'admins'])->get('admin:1'); // Remember with tags $users = Cache::tags(['users'])->remember('all', 600, function () { return User::all(); }); // Flush specific tag Cache::tags(['users'])->flush(); // Flush multiple tags Cache::tags(['users', 'admins'])->flush();
Warning: Cache tags are not supported by the file or database cache drivers. Only use tags with Redis or Memcached.

Atomic Locks

Prevent race conditions using atomic locks:

use Illuminate\Support\Facades\Cache; // Basic lock $lock = Cache::lock('process-orders', 10); // 10 second expiration if ($lock->get()) { // Lock acquired, process orders processOrders(); // Release lock $lock->release(); } // Block until lock is available $lock = Cache::lock('process-orders', 10); $lock->block(5, function () { // Lock acquired after waiting up to 5 seconds processOrders(); }); // Auto-release with callback Cache::lock('process-orders')->get(function () { // Lock acquired and will auto-release processOrders(); }); // Check lock ownership if ($lock->owner() === 'unique-id') { $lock->release(); } // Force release $lock->forceRelease();

Cache Helper Functions

Laravel provides convenient cache helper functions:

// Using cache() helper cache(['key' => 'value'], 600); // Store $value = cache('key'); // Retrieve cache()->remember('users', 600, function () { return User::all(); }); // Using global functions cache_put('key', 'value', 600); $value = cache_get('key'); cache_forget('key'); cache_flush();

Model Query Caching

Cache database query results effectively:

// Cache model query $users = Cache::remember('users:active', 600, function () { return User::where('active', true)->get(); }); // Cache single model $user = Cache::remember("user:{$id}", 600, function () use ($id) { return User::findOrFail($id); }); // Cache with relationships $posts = Cache::remember('posts:recent', 600, function () { return Post::with('author', 'comments') ->latest() ->take(10) ->get(); }); // Cache count queries $count = Cache::remember('users:count', 3600, function () { return User::count(); }); // Invalidate cache on model events class User extends Model { protected static function boot() { parent::boot(); static::saved(function () { Cache::forget('users:active'); Cache::forget('users:count'); }); static::deleted(function () { Cache::forget('users:active'); Cache::forget('users:count'); }); } }

Rate Limiting with Cache

Implement rate limiting using cache:

use Illuminate\Support\Facades\RateLimiter; // Configure rate limiter RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // Check rate limit if (RateLimiter::tooManyAttempts('send-message:' . $user->id, 5)) { $seconds = RateLimiter::availableIn('send-message:' . $user->id); return response()->json([ 'message' => "Too many requests. Try again in {$seconds} seconds." ], 429); } // Hit rate limiter RateLimiter::hit('send-message:' . $user->id, 60); // 60 second decay // Custom rate limiting $executed = RateLimiter::attempt( 'send-email:' . $user->id, $maxAttempts = 5, function () { // Send email }, $decaySeconds = 60 ); if (!$executed) { return 'Too many emails sent!'; } // Clear rate limiter RateLimiter::clear('send-message:' . $user->id);

View Caching

Cache compiled Blade views for better performance:

# Cache all views php artisan view:cache # Clear view cache php artisan view:clear

Cache view fragments within Blade templates:

@cache('sidebar', 600) <div class="sidebar"> @foreach(Category::all() as $category) <a href="{{ $category->url }}">{{ $category->name }}</a> @endforeach </div> @endcache {{-- Custom cache directive (create in service provider) --}} Blade::directive('cache', function ($expression) { return "<?php if(! cache()->has({$expression})): cache()->put({$expression}, ob_start()); ?>"; }); Blade::directive('endcache', function () { return "<?php cache()->put( array_shift(func_get_args()), ob_get_clean() ); endif; ?>"; });

Advanced Caching Strategies

// Cache-Aside Pattern public function getUser($id) { return Cache::remember("user:{$id}", 600, function () use ($id) { return User::find($id); }); } // Write-Through Pattern public function updateUser($id, array $data) { $user = User::findOrFail($id); $user->update($data); Cache::put("user:{$id}", $user, 600); return $user; } // Cache Warming public function warmCache() { $popularProducts = Product::popular()->get(); Cache::put('products:popular', $popularProducts, 3600); $categories = Category::with('products')->get(); Cache::put('categories:tree', $categories, 3600); } // Stale-While-Revalidate Pattern public function getProducts() { $products = Cache::get('products'); if (!$products) { $products = Product::all(); Cache::put('products', $products, 600); } // Revalidate in background if cache is old if (Cache::get('products:timestamp') < now()->subMinutes(5)) { dispatch(new RefreshProductsCache()); Cache::put('products:timestamp', now(), 600); } return $products; } // Multi-Level Caching public function getUser($id) { // Level 1: Application cache (Redis) $user = Cache::get("user:{$id}"); if (!$user) { // Level 2: Query result cache $user = DB::table('users') ->where('id', $id) ->remember(600) ->first(); Cache::put("user:{$id}", $user, 600); } return $user; }
Tip: Use cache tagging to organize related cache entries. When a user updates their profile, flush all user-related caches: Cache::tags(['user:' . $id])->flush()

Cache Performance Monitoring

// Log cache hits and misses Cache::spy(); Event::listen('cache:hit', function ($event) { \Log::info('Cache hit', ['key' => $event->key]); }); Event::listen('cache:missed', function ($event) { \Log::info('Cache miss', ['key' => $event->key]); }); // Monitor cache performance $start = microtime(true); $value = Cache::get('expensive-query'); $time = microtime(true) - $start; if ($time > 0.1) { \Log::warning('Slow cache retrieval', [ 'key' => 'expensive-query', 'time' => $time, ]); }
Exercise 1: Implement a comprehensive caching strategy for a blog. Cache post lists, individual posts with comments, category trees, and popular posts. Include automatic cache invalidation when posts are created, updated, or deleted.
Exercise 2: Build a rate limiter for an API using cache. Implement per-user rate limits (100 requests/hour), IP-based limits for anonymous users (20 requests/hour), and return proper 429 responses with Retry-After headers.
Exercise 3: Create a cache warming system that pre-populates cache with frequently accessed data during off-peak hours. Use scheduled tasks to refresh cache for homepage data, popular products, and navigation menus every 30 minutes.

Cache Best Practices

Best Practices:
  • Use appropriate TTL: Short TTL for dynamic data (5-10 min), long TTL for static data (hours/days)
  • Cache at multiple layers: HTTP cache, application cache, database query cache
  • Invalidate smartly: Use tags or patterns to invalidate related caches together
  • Monitor cache hit ratio: Aim for 80%+ hit ratio for effective caching
  • Avoid cache stampede: Use locks when regenerating expensive cached data
  • Cache serializable data: Don't cache resources or closures
  • Use Redis for production: It's fast, reliable, and supports advanced features
  • Warm critical caches: Pre-populate cache before traffic spikes

Effective caching is crucial for building high-performance Laravel applications. By implementing proper caching strategies, you can significantly reduce database load, improve response times, and provide a better user experience. This concludes our Laravel Framework tutorial series!