0x55aa
โ† Back to Blog

Laravel Cache: Stop Hitting the Database Every Single Time ๐Ÿš€๐Ÿ’พ

โ€ข8 min read

Laravel Cache: Stop Hitting the Database Every Single Time ๐Ÿš€๐Ÿ’พ

Your users just complained that the app is slow. You check New Relic. 47 database queries. PER PAGE LOAD. Your database is sweating harder than me at the gym. ๐Ÿ’ฆ

Let me guess - you're fetching the same categories/settings/user permissions on every single request? Yeah, we've all been there. Time to talk about caching.

The Problem: Death by a Thousand Queries ๐ŸŒ

Here's what I see in code reviews ALL THE TIME:

// This runs on EVERY page load
public function index()
{
    $categories = Category::with('subcategories')->get();
    $settings = Setting::all();
    $activeUsers = User::where('active', true)->count();

    return view('dashboard', compact('categories', 'settings', 'activeUsers'));
}

Looks innocent, right? Wrong. This data probably changes once a week, but you're hitting the database 100+ times a day for it. Your database hates you. Your users hate you. Your hosting bill hates you. ๐Ÿ˜…

Real Talk: The Production Meltdown ๐Ÿ’ฌ

In production systems I've built at Cubet, we had an e-commerce API serving 10k+ requests/hour. Every product listing page was querying categories, brands, filters, settings - the same data, over and over.

Before caching:

  • Response time: 800ms average
  • Database CPU: 85% constantly
  • Server costs: Too embarrassing to mention

After implementing smart caching:

  • Response time: 150ms average
  • Database CPU: 15% normal operation
  • Server costs: Cut in HALF

We literally saved thousands of dollars per month. That's the power of caching done right. ๐Ÿ’ฐ

The Cache Facade: Your New Best Friend ๐ŸŽฏ

Laravel makes caching stupidly simple. Here's the basic pattern:

use Illuminate\Support\Facades\Cache;

// Store something
Cache::put('key', 'value', $seconds);

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

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

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

// Remove from cache
Cache::forget('key');

Easy, right? But the REAL magic is in how you use it.

Pattern #1: Remember Forever (Until You Don't) ๐Ÿ”„

For data that rarely changes:

// Before (hits DB every time)
public function getCategories()
{
    return Category::with('subcategories')->get();
}

// After (hits DB once, then cached)
public function getCategories()
{
    return Cache::remember('categories', 3600, function () {
        return Category::with('subcategories')->get();
    });
}

The remember() method is GORGEOUS:

  1. Check if 'categories' exists in cache
  2. If yes โ†’ return it (fast!)
  3. If no โ†’ run the closure, cache the result, return it

One method. One line change. Massive performance boost. This is why I love Laravel. โค๏ธ

Pattern #2: Cache Tags (The Organized Hoarder) ๐Ÿท๏ธ

As a Technical Lead, I've learned that cache invalidation is the hardest part. You can't just cache everything forever - data gets stale.

Enter cache tags (requires Redis or Memcached):

// Cache with tags
Cache::tags(['products', 'featured'])->put('featured_products', $products, 3600);
Cache::tags(['products', 'sale'])->put('sale_products', $products, 3600);

// Flush all product-related caches at once
Cache::tags(['products'])->flush();

// Or just flush featured items
Cache::tags(['featured'])->flush();

A pattern that saved us in a real project: When a product was updated, we flushed ALL product caches by tag. No need to remember every cache key. Just Cache::tags(['products'])->flush() and boom - all product caches gone. ๐ŸŽฏ

Pattern #3: The Model Observer Hook ๐Ÿช

Want automatic cache invalidation? Hook into model events:

// app/Observers/CategoryObserver.php
namespace App\Observers;

use App\Models\Category;
use Illuminate\Support\Facades\Cache;

class CategoryObserver
{
    public function saved(Category $category)
    {
        // Clear cache whenever category changes
        Cache::forget('categories');
        Cache::tags(['categories'])->flush();
    }

    public function deleted(Category $category)
    {
        Cache::forget('categories');
        Cache::tags(['categories'])->flush();
    }
}

// Register in AppServiceProvider
Category::observe(CategoryObserver::class);

Now your cache AUTOMATICALLY stays fresh. Update a category? Cache clears. Delete a category? Cache clears. You literally don't have to think about it anymore. Set it and forget it! ๐Ÿ”ฅ

Pattern #4: The Atomic Lock (Race Condition Savior) ๐Ÿ”

Ever had two requests hit your cache at the EXACT same time when it's empty? Both start rebuilding the cache. Both hit the database. Chaos ensues.

Laravel has you covered:

// Only ONE request builds the cache
$value = Cache::lock('expensive-operation')->get(function () {
    return Cache::remember('expensive-data', 3600, function () {
        // This only runs ONCE even if 1000 requests hit simultaneously
        return $this->veryExpensiveCalculation();
    });
});

I've seen this save production systems during traffic spikes. Without the lock, we'd have 100 concurrent requests all rebuilding the same cache. With the lock? One request does the work, 99 wait a few milliseconds. Beautiful. ๐Ÿ˜

Pro Tips from the Trenches ๐Ÿ’ก

1. Cache Driver Matters

// .env
CACHE_DRIVER=redis  // FAST (use in production)
// CACHE_DRIVER=file     // Okay for local dev
// CACHE_DRIVER=database // Why would you do this?

In production, use Redis or Memcached. File/database caching defeats the purpose. You're trying to AVOID hitting the database, remember?

2. Don't Cache User-Specific Data Globally

// BAD - caches for ALL users
Cache::remember('user_settings', 3600, function () {
    return auth()->user()->settings;
});

// GOOD - cache per user
Cache::remember("user_settings_{$userId}", 3600, function () use ($userId) {
    return User::find($userId)->settings;
});

Made this mistake once. User A saw User B's settings. That was a fun bug report. ๐Ÿคฆโ€โ™‚๏ธ

3. Cache Warming

Don't wait for users to hit empty cache:

// artisan command to warm up cache
public function handle()
{
    Cache::remember('categories', 3600, fn() => Category::all());
    Cache::remember('settings', 3600, fn() => Setting::all());
    Cache::remember('popular_products', 3600, fn() => Product::popular()->get());

    $this->info('Cache warmed successfully!');
}

// Run after deployment
php artisan cache:warm

4. Remember to Clear Cache on Deployment

# In your deployment script
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache

I've debugged SO many "why isn't my change showing?" issues that were just cached config. Clear that cache! ๐Ÿงน

The "Cache Everything" Anti-Pattern โš ๏ธ

Don't cache EVERYTHING just because you can:

Don't cache:

  • User-specific real-time data (notifications, messages)
  • Frequently changing data (stock prices, live scores)
  • Small, fast queries (single row lookups by ID)
  • Data that MUST be real-time (payment status, inventory counts)

DO cache:

  • Navigation menus, categories
  • Site settings, configurations
  • Computed aggregations (counts, stats)
  • External API responses
  • Complex queries with joins

As a rule: If the query takes < 10ms, caching might not be worth the complexity. If it takes > 100ms, DEFINITELY cache it.

Real-World Example: Product Listing Page ๐Ÿ›๏ธ

Here's a before/after from an actual e-commerce project:

Before:

public function index(Request $request)
{
    $categories = Category::all(); // Query 1
    $brands = Brand::all(); // Query 2
    $filters = Filter::with('options')->get(); // Query 3+
    $products = Product::with('images', 'variants')
        ->where('active', true)
        ->paginate(24); // Query 4+

    return view('products.index', compact('categories', 'brands', 'filters', 'products'));
}
// Total: ~15 queries, 300ms response time

After:

public function index(Request $request)
{
    $categories = Cache::remember('categories', 3600,
        fn() => Category::all()
    );

    $brands = Cache::remember('brands', 3600,
        fn() => Brand::all()
    );

    $filters = Cache::remember('filters', 3600,
        fn() => Filter::with('options')->get()
    );

    // Don't cache paginated results (they change per page)
    $products = Product::with('images', 'variants')
        ->where('active', true)
        ->paginate(24);

    return view('products.index', compact('categories', 'brands', 'filters', 'products'));
}
// Total: ~3 queries (first hit), ~1 query (cached), 80ms response time

80% faster. Same functionality. Five minutes of work. ๐Ÿš€

Monitoring Your Cache ๐Ÿ“Š

Track cache hit rates to know if caching is working:

// Custom middleware to track hits/misses
public function handle($request, Closure $next)
{
    $key = "page_cache_{$request->path()}";

    if (Cache::has($key)) {
        Log::info('Cache HIT', ['key' => $key]);
    } else {
        Log::info('Cache MISS', ['key' => $key]);
        Cache::put($key, true, 60);
    }

    return $next($request);
}

If you're seeing mostly misses, your TTL is too short or you're caching the wrong things.

The Bottom Line

Caching isn't just about speed - it's about scalability. A cached app can handle 10x the traffic without breaking a sweat (or your bank account).

Your Cache Action Plan:

  1. Identify slow, repeated queries (check your logs!)
  2. Cache them with appropriate TTLs
  3. Set up cache invalidation (tags or observers)
  4. Monitor cache hit rates
  5. Profit (literally - lower hosting costs!)

In production systems I've architected, proper caching has been the difference between handling 100 requests/second and 1000 requests/second. Same hardware. Same code. Just smarter data management.

The Cache Commandments ๐Ÿ“œ

โœ… Cache data that's expensive to fetch โœ… Use tags for organized invalidation โœ… Clear cache on model changes โœ… Use Redis/Memcached in production โœ… Cache warmup after deployments

โŒ Don't cache user-specific data globally โŒ Don't cache real-time critical data โŒ Don't forget to clear cache on deploy โŒ Don't use file/database drivers in prod โŒ Don't cache without a plan to invalidate


Your database will thank you. Your users will thank you. Your hosting bill will DEFINITELY thank you. ๐Ÿ’ฐ

Questions about caching? Hit me up on LinkedIn - I've probably cached that answer already! ๐Ÿ˜„

Want more performance tips? Check out my Laravel Performance Tips post!

Now go cache ALL the things! ๐Ÿš€๐Ÿ’พ