Laravel Caching: Making Your App Fly

SS Saurav Sitaula

Database queries are expensive. API calls are slow. Learn to cache the expensive stuff — with Redis, Memcached, or just files. Cache tags, cache invalidation, and the patterns that made my Laravel apps actually fast.

The Slow Dashboard Problem

My admin dashboard loaded in 8 seconds. Every page load ran:

  • 15 database queries for stats
  • 3 API calls to external services
  • Complex aggregations on millions of rows

Users complained. I profiled. The numbers didn’t lie — most time was spent waiting for data that rarely changed.

Solution? Don’t fetch it every time. Cache it.

The Cache Facade

use Illuminate\Support\Facades\Cache;

// Store a value for 60 minutes
Cache::put('key', 'value', 60);

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

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

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

// Check existence
if (Cache::has('key')) {
    // ...
}

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

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

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

The Magic Method: remember()

This is the pattern I use 90% of the time:

$users = Cache::remember('all_users', 60, function () {
    return User::all();
});

This does:

  1. Check if all_users exists in cache
  2. If yes, return the cached value
  3. If no, run the closure, cache the result for 60 minutes, return it

One line replaces the check-fetch-store dance.

Forever Version

$settings = Cache::rememberForever('site_settings', function () {
    return Setting::all()->pluck('value', 'key');
});

Cache until manually cleared. Good for data that rarely changes.

Cache Drivers

Configure in config/cache.php:

'default' => env('CACHE_DRIVER', 'file'),

'stores' => [
    'file' => [
        'driver' => 'file',
        'path' => storage_path('framework/cache/data'),
    ],
    
    'redis' => [
        'driver' => 'redis',
        'connection' => 'cache',
    ],
    
    'memcached' => [
        'driver' => 'memcached',
        'servers' => [
            [
                'host' => env('MEMCACHED_HOST', '127.0.0.1'),
                'port' => env('MEMCACHED_PORT', 11211),
                'weight' => 100,
            ],
        ],
    ],
    
    'array' => [
        'driver' => 'array',  // For testing, doesn't persist
    ],
],

Development: file driver is fine. Production: redis or memcached for speed.

Switch with one env variable:

CACHE_DRIVER=redis

Using Redis

Install Redis and the PHP extension:

sudo apt install redis-server
composer require predis/predis

Configure in .env:

CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

Now all Cache:: calls use Redis. Faster than file, shared across servers.

Practical Caching Patterns

Cache Database Queries

// Without caching - runs query every time
public function index()
{
    $posts = Post::with('user', 'tags')
        ->published()
        ->latest()
        ->paginate(20);
    
    return view('posts.index', compact('posts'));
}

// With caching - query runs once per 10 minutes
public function index()
{
    $page = request('page', 1);
    
    $posts = Cache::remember("posts_page_{$page}", 10, function () use ($page) {
        return Post::with('user', 'tags')
            ->published()
            ->latest()
            ->paginate(20);
    });
    
    return view('posts.index', compact('posts'));
}

Cache API Calls

public function getExchangeRates()
{
    return Cache::remember('exchange_rates', 60, function () {
        // Only calls API once per hour
        $response = Http::get('https://api.exchangerate.io/latest');
        return $response->json();
    });
}

Cache Expensive Computations

public function getMonthlyReport($month)
{
    return Cache::remember("report_{$month}", 1440, function () use ($month) {
        // Complex aggregation that takes 30 seconds
        return Order::whereMonth('created_at', $month)
            ->selectRaw('DATE(created_at) as date, SUM(total) as revenue, COUNT(*) as orders')
            ->groupBy('date')
            ->get();
    });
}

Cache User-Specific Data

public function getDashboard()
{
    $userId = auth()->id();
    
    $stats = Cache::remember("user_{$userId}_stats", 5, function () use ($userId) {
        return [
            'orders_count' => Order::where('user_id', $userId)->count(),
            'total_spent' => Order::where('user_id', $userId)->sum('total'),
            'pending_orders' => Order::where('user_id', $userId)->pending()->count(),
        ];
    });
    
    return view('dashboard', compact('stats'));
}

Cache Invalidation

“There are only two hard things in Computer Science: cache invalidation and naming things.”

Manual Invalidation

// When a post is updated
public function update(Request $request, Post $post)
{
    $post->update($request->validated());
    
    // Clear related caches
    Cache::forget('posts_page_1');
    Cache::forget("post_{$post->id}");
    Cache::forget('recent_posts');
    
    return redirect()->back();
}

Using Model Observers

// app/Observers/PostObserver.php
class PostObserver
{
    public function saved(Post $post)
    {
        Cache::forget("post_{$post->id}");
        $this->clearPostListCaches();
    }

    public function deleted(Post $post)
    {
        Cache::forget("post_{$post->id}");
        $this->clearPostListCaches();
    }

    private function clearPostListCaches()
    {
        // Clear first 10 pages
        for ($i = 1; $i <= 10; $i++) {
            Cache::forget("posts_page_{$i}");
        }
        Cache::forget('recent_posts');
        Cache::forget('popular_posts');
    }
}

Cache Tags (Redis/Memcached only)

Tags let you group related cache items:

// Store with tags
Cache::tags(['posts', 'users'])->put("post_{$id}", $post, 60);
Cache::tags(['posts'])->put('recent_posts', $recent, 60);
Cache::tags(['users'])->put("user_{$id}", $user, 60);

// Flush all caches with a tag
Cache::tags(['posts'])->flush();  // Clears all post-related caches

Much cleaner than tracking individual keys.

class PostObserver
{
    public function saved(Post $post)
    {
        Cache::tags(['posts'])->flush();
    }
}

One line clears everything related to posts.

Cache Lock (Preventing Race Conditions)

What if two requests try to build the same cache simultaneously?

// Without lock - might run expensive query twice
$posts = Cache::remember('posts', 60, function () {
    return $this->expensiveQuery();
});

// With lock - only one request runs the query
$posts = Cache::lock('posts_lock', 10)->block(5, function () {
    return Cache::remember('posts', 60, function () {
        return $this->expensiveQuery();
    });
});

The lock ensures only one process builds the cache. Others wait.

Cache in Blade

Cache entire view fragments:

@cache('sidebar_' . auth()->id(), 60)
    <div class="sidebar">
        {{-- Expensive sidebar content --}}
        @foreach($recommendations as $item)
            <div class="recommendation">{{ $item->title }}</div>
        @endforeach
    </div>
@endcache

This requires the laravel-responsecache or blade-cache package, but the concept is powerful.

Artisan Cache Commands

# Clear application cache
php artisan cache:clear

# Clear config cache
php artisan config:clear

# Clear route cache
php artisan route:clear

# Clear view cache
php artisan view:clear

# Clear all caches
php artisan optimize:clear

# Cache config for production (faster boot)
php artisan config:cache

# Cache routes for production
php artisan route:cache

Cache Best Practices

1. Key Naming Convention

// Bad
Cache::put('data', $value);
Cache::put('stuff', $value);

// Good - descriptive, namespaced
Cache::put('posts:page:1', $value);
Cache::put('users:123:profile', $value);
Cache::put('api:weather:nyc:2019-07-15', $value);

2. Set Appropriate TTLs

// Very short - data changes frequently
Cache::put('active_users_count', $count, 1);  // 1 minute

// Medium - data changes occasionally
Cache::put('popular_posts', $posts, 30);  // 30 minutes

// Long - data rarely changes
Cache::put('site_settings', $settings, 1440);  // 24 hours

// Forever - static reference data
Cache::forever('countries_list', $countries);

3. Always Have a Fallback

// Handle cache failures gracefully
try {
    $data = Cache::remember('key', 60, function () {
        return $this->getData();
    });
} catch (\Exception $e) {
    // Cache server down? Fall back to direct query
    $data = $this->getData();
    Log::warning('Cache unavailable', ['error' => $e->getMessage()]);
}

4. Don’t Cache Everything

Good candidates for caching:

  • Database queries that run often with same parameters
  • External API responses
  • Expensive computations
  • Aggregated data

Bad candidates:

  • User-specific data that changes constantly
  • Small, fast queries
  • Data that must be real-time

My Dashboard Fix

Before: 8 seconds. After: 200ms.

public function dashboard()
{
    return view('admin.dashboard', [
        'stats' => Cache::remember('admin:dashboard:stats', 5, function () {
            return [
                'total_users' => User::count(),
                'total_orders' => Order::count(),
                'revenue_today' => Order::whereDate('created_at', today())->sum('total'),
                'pending_orders' => Order::pending()->count(),
            ];
        }),
        
        'chart_data' => Cache::remember('admin:dashboard:chart', 60, function () {
            return Order::selectRaw('DATE(created_at) as date, SUM(total) as total')
                ->whereBetween('created_at', [now()->subDays(30), now()])
                ->groupBy('date')
                ->get();
        }),
        
        'recent_orders' => Cache::remember('admin:dashboard:recent', 1, function () {
            return Order::with('user')->latest()->take(10)->get();
        }),
    ]);
}

Stats cache for 5 minutes. Chart caches for 1 hour. Recent orders cache for 1 minute.

What I Wish I’d Known Earlier

  1. Cache::remember() is your best friend. Use it everywhere.

  2. Cache keys should be predictable. If you can’t easily construct the key, you can’t easily invalidate it.

  3. Tags are worth the Redis dependency. Invalidation becomes trivial.

  4. Profile before caching. Don’t cache things that aren’t slow.

  5. Clear cache on deployment. Add php artisan cache:clear to your deploy script.

The Journey Continues

Caching made my app fast. But there was still repetitive work — sending reminders, cleaning up old files, generating reports. Things that needed to run on a schedule.

Laravel had a beautiful solution for that too.


P.S. — The dashboard that took 8 seconds now loads in under 200ms. Users thought we’d upgraded the server. Nope — just caching. Sometimes the biggest performance wins come from NOT doing work rather than doing it faster.

SS

Saurav Sitaula

Software Architect • Nepal

Back to all posts
Saurav.dev

© 2026 Saurav Sitaula.AstroNeoBrutalism