Database Query Caching Layers: Optimizing Laravel Performance
Learn how to implement database query caching in Laravel to reduce server load. Master cache eviction strategies to ensure data integrity in high-traffic apps.
Previously in this course, we explored Database Indexing Strategies: Optimizing Laravel Query Performance. While indexing is your first line of defense against slow queries, even the most optimized B-Tree index cannot save you from an expensive, repetitive query hitting the database millions of times per day. This lesson adds a layer of intelligence to your data access by implementing caching at the query level.
The First Principles of Query Caching
Database query caching is the act of storing the result set of an expensive SQL operation in a fast, in-memory key-value store (like Redis or Memcached). Instead of asking the database to perform the same heavy computation—such as complex aggregations or deep relationship loading—your application checks the cache first.
Think of it as a two-tier lookup:
- Cache Layer: Does the key for this specific query result exist? If yes, return it immediately.
- Database Layer: If no, execute the SQL, serialize the result, store it in the cache with a defined TTL (Time-To-Live), and return it.
Implementing Query Result Caching
In Laravel, we leverage the remember() method on the Query Builder or Eloquent. This is the cleanest way to wrap execution logic without cluttering your services.
Worked Example: Caching Expensive Aggregations
Imagine our SaaS platform has a dashboard widget showing the "Top 100 Revenue-Generating Customers" over the last 30 days. This involves complex joins and sum calculations.
PHPnamespace App\Services\Billing; use App\Models\Customer; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; class RevenueService { public function getTopCustomers(int $days = 30) { $cacheKey = "top_customers_last_{$days}_days"; return Cache::remember($cacheKey, now()->addMinutes(60), function () use ($days) { return Customer::query() ->select('customers.*') ->join('orders', 'customers.id', '=', 'orders.customer_id') ->where('orders.created_at', '>=', now()->subDays($days)) ->groupBy('customers.id') ->orderByRaw('SUM(orders.amount) DESC') ->limit(100) ->get(); }); } }
By adding this layer, we shift the burden from the database CPU to the Redis memory. The query only hits the database once per hour, regardless of how many thousands of users load the dashboard.
Handling Cache Eviction
The biggest risk with caching is "stale data." If a customer updates their profile or a new order is placed, your cache will still serve the old, cached result until the TTL expires. You must implement proactive cache eviction.
The most robust approach is to tie cache clearing to model events.
PHP#6A9955">// In your Service Provider or a dedicated Listener Customer::updated(function ($customer) { Cache::forget("top_customers_last_30_days"); });
For more complex scenarios where multiple queries depend on the same dataset, consider using Cache Tags (if using Redis or Memcached) to flush groups of related queries at once.
Common Pitfalls
- Over-caching: Do not cache every query. If a query is fast (e.g., fetching a user by ID via a primary key index), the overhead of serializing and deserializing from Redis might actually be slower than the database query itself.
- Cache Stampedes: If a highly popular key expires, multiple concurrent requests might try to re-generate the cache simultaneously, hitting the database all at once. Use a "grace period" or atomic locks if your traffic is extreme.
- Ignoring Serialization Limits: Eloquent objects can be large. When you cache a collection of 1,000 models, you are storing the entire object graph in Redis. If memory is tight, consider caching only the necessary IDs or using a DTO instead of the full Eloquent model.
Practice Exercise
- Identify one "heavy" query in your project's
BillingorReportingdomain. - Implement
Cache::remember()with a 15-minute TTL. - Verify the query is cached by logging the database queries (using
DB::listen) and checking if the logs stop appearing on subsequent page refreshes. - Implement an event listener that clears this specific cache key when relevant data changes.
Recap
Database query caching is a powerful tool to scale your SaaS, but it requires discipline. By wrapping your most expensive SQL queries in Cache::remember() and meticulously managing eviction via model events, you significantly reduce the load on your primary database. Always prioritize indexing first, as shown in our earlier modules on PostgreSQL Indexing: B-Tree vs GIN for Better Query Performance, and use caching as your second-tier optimization.
Up next: We will dive into Advanced Eloquent Scopes to make these complex queries reusable and maintainable across your domain services.
Work with me

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.