Back to Blog
Lesson 29 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20264 min read

Caching Strategies for Performance: Mastering Laravel and Redis

Learn how to use caching in Laravel to slash database load. Master cache tags, store configuration, and invalidation strategies for high-performance apps.

laravelredisperformancecachingbackendphp

Previously in this course, we covered Eloquent Performance Optimization to eliminate N+1 query issues. While eager loading is a mandatory first step, some operations—like calculating complex project statistics or fetching global settings—remain expensive no matter how well you optimize your SQL.

Today, we move beyond the database layer by implementing caching. We’ll focus on using Redis to store computed results, using cache tags for granular control, and ensuring your cache stays fresh through proper invalidation.

Why Caching Matters

Every time a user requests your project board, the server performs a round-trip to the database, deserializes rows into Eloquent models, and transforms them into JSON. On a high-traffic app, this is a bottleneck.

Caching is the practice of storing the result of an expensive operation in a fast, volatile storage medium (like RAM) so subsequent requests can skip the heavy lifting. In Laravel, we use the Cache facade to abstract this process, allowing us to swap drivers like file, database, or redis without changing our application logic.

Using Cache Stores and Redis

Laravel allows you to define multiple stores in config/cache.php. For production, Redis is the industry standard because it’s an in-memory data structure store that supports atomic operations and complex data types.

To use Redis, ensure you have the predis/predis or phpredis extension installed and update your .env:

Bash
CACHE_STORE=redis
REDIS_CLIENT=phpredis

Now, you can interact with the store directly:

PHP
#6A9955">// Storing data for 60 minutes
Cache::store('redis')->put('project_stats_1', $data, now()->addMinutes(60));

#6A9955">// Retrieving with a fallback closure
$stats = Cache::store('redis')->remember('project_stats_1', 3600, function () {
    return Project::calculateComplexStats();
});

Implementing Cache Tags

A common mistake is caching everything under a single key. When the underlying data changes, you're forced to clear the entire cache. Cache tags solve this by allowing you to group related cache items and purge them collectively.

Note: Cache tags are only supported by drivers like redis or memcached.

Imagine our project board has a list of tasks. We want to cache the tasks for a specific project, but we also want to cache them under a "tasks" tag so we can clear all task-related caches if a global update occurs.

PHP
#6A9955">// Tagging cache items
Cache::tags(['projects', 'tasks'])->put('project_1_tasks', $tasks, 3600);

#6A9955">// Retrieving tagged items
$tasks = Cache::tags(['projects', 'tasks'])->get('project_1_tasks');

#6A9955">// Invalidating everything under the 'projects' tag
Cache::tags(['projects'])->flush();

Invalidation: The "Hard" Problem

Caching is easy; cache invalidation is where most engineers fail. If you cache a project’s task list but don't clear it when a task is added, the user sees stale data.

We solve this using observers or events. In our project board, every time a Task is created or updated, we must clear the relevant cache.

PHP
#6A9955">// Inside a TaskObserver
public function saved(Task $task)
{
    #6A9955">// Clear the specific project cache tag
    Cache::tags(['project_' . $task->project_id])->flush();
}

This pattern ensures that the cache is always eventually consistent with the database. You can read more about managing complex dependencies in Database Caching Strategies: Mastering Partitioned Keys and Eviction to see how this scales.

Hands-on Exercise

In our ongoing project board, open your TaskService.

  1. Identify the method that fetches the list of tasks for a project.
  2. Wrap that query in a Cache::tags(['project_' . $projectId])->remember(...) block.
  3. Create a TaskObserver and use the saved and deleted hooks to flush the project_{id} tag.
  4. Verify that the database query count drops when refreshing the page after the first load.

Common Pitfalls

  1. Caching dynamic data: Never cache data that changes every second (like a user's current session or CSRF tokens). Use cache for read-heavy, write-rare data.
  2. Ignoring Serialization: When using Redis, Laravel serializes your objects. Ensure your data is serializable (avoid caching open database connections or file handles).
  3. Over-caching: Caching everything leads to "cache stampedes," where your cache becomes more complex to maintain than your database queries. Cache only what is demonstrably slow.
  4. Forgetting Time-To-Live (TTL): Always set an expiration. If you don't, your cache will grow until your Redis memory limit is reached, causing it to evict keys unpredictably.

Recap

Performance requires a multi-layered approach. By moving expensive calculations into Redis using cache tags, we reduce the load on our primary database. Remember: cache invalidation is the most critical part of the strategy—if your data is stale, the performance gain is irrelevant.

Up next: We’ll explore how to clean up our code further by Using Traits for Code Reuse, allowing us to share common caching and logic patterns across our models and services.

Similar Posts