Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 032 / 100

Caching in Laravel: a PHP & Laravel guide for developers

Speed up slow queries and pages.

Expona Admin6 min read

Why caching matters in Laravel

When your dashboard queries take three seconds to load or your product listing needs to hit the database on every page view, users notice. Caching in Laravel is a PHP & Laravel guide for developers who want to eliminate these bottlenecks without rewriting their queries or denormalizing their schemas. We've all hit that moment where a single poorly optimized query cascades across your entire application—caching is how we recover.

Laravel's caching layer abstracts the complexity of managing in-memory stores, query results, and cache invalidation. Whether you're running a high-traffic SaaS platform or a moderately busy e-commerce site, understanding how to cache effectively is the difference between a responsive application and one that drowns under load.

Cache::remember — Compute once, reuse until expiry

The most practical caching pattern we use in production is Cache::remember(). This method checks if a key exists; if it does, it returns the cached value. If not, it executes a closure, stores the result, and returns it. This single-responsibility pattern keeps your code clean and your logic isolated.

php
// Dashboard statistics that take 2 seconds to compute
$stats = Cache::remember('dashboard', 300, fn () => Stats::compute());

// 300 seconds = 5 minutes. After expiry, it recomputes on next request.

The closure only runs when the cache misses. On subsequent requests within the TTL window, you get the stored result instantly. This is ideal for aggregations, expensive joins, or computations that don't need to be perfectly real-time.

We commonly use Cache::remember() in three scenarios:

  • Dashboard metrics: Monthly revenue, user signups, churn rate—compute once per hour.
  • Leaderboards: Top performers ranked by points or sales, refreshed every 30 minutes.
  • Product listings: Filtered or sorted results from complex queries, cached for 15 minutes.

The key is choosing the right TTL. Too short and you waste CPU on recomputation. Too long and stale data frustrates users. In production, we often start with a conservative TTL and adjust based on monitoring.

php
// Example: product recommendations
$recommendations = Cache::remember(
    "recommendations.user.{$userId}",
    600, // 10 minutes
    fn () => Product::recommendedFor($userId)->get()
);

// The key includes the user ID, so each user gets their own cache entry.

Redis driver — Use Redis in production for speed and tags

Laravel ships with several cache drivers: array, database, file, memcached, and redis. For production workloads, Redis is our choice. It's faster than file-based caching, more reliable than in-process arrays, and scales horizontally.

Configure it in your config/cache.php:

php
// config/cache.php
'redis' => [
    'driver' => 'redis',
    'connection' => 'cache', // uses config/database.php redis config
    'lock_connection' => 'default',
],

Then in config/database.php, define your Redis connection:

php
'redis' => [
    'cache' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_CACHE_PORT', 6379),
        'database' => env('REDIS_CACHE_DB', 1),
    ],
],

Redis gives you atomic operations, persistence options, and the ability to inspect your cache in real-time using redis-cli. In production clusters, Redis Cluster or Sentinel handles failover and replication automatically.

Switch your driver by setting CACHE_DRIVER=redis in your .env file. All your existing cache calls remain unchanged—Laravel abstracts the driver implementation.

Imagine you cache user profiles, their order history, and their notification count separately. When a user's profile updates, you need to invalidate all three. Manually flushing each key is error-prone. This is where cache tags solve the problem.

Tags let you group related cache entries and flush them atomically:

php
// Store data with tags
Cache::tags(['user:123', 'profile'])->put('user.123.profile', $profile, 600);
Cache::tags(['user:123', 'orders'])->put('user.123.orders', $orders, 600);
Cache::tags(['user:123', 'notifications'])->put('user.123.notifications', $count, 600);

// Retrieve tagged data
$profile = Cache::tags(['user:123', 'profile'])->get('user.123.profile');

// Flush all entries tagged with 'user:123'
Cache::tags(['user:123'])->flush();

// Or flush everything tagged 'profile' (across all users)
Cache::tags(['profile'])->flush();

Tags only work with drivers that support them: Redis and Memcached. File and database drivers ignore tags silently, so always test your cache layer in a production-like environment.

We use tags heavily in multi-tenant systems where each tenant's data is isolated:

php
// In a tenant context
Cache::tags(["tenant:{$tenantId}", 'dashboard'])->remember(
    'dashboard.stats',
    300,
    fn () => Stat::forTenant($tenantId)->compute()
);

// When tenant data changes
Cache::tags(["tenant:{$tenantId}"])->flush();

This pattern ensures that when you update a tenant's configuration or data, all their cached entries evaporate in one operation, and no stale data leaks between requests.

Config and route caching — php artisan optimize on deploy

Beyond data caching, Laravel caches your application's configuration and routes at boot time. During development, Laravel reads config/ files and route definitions from disk on every request. In production, this overhead accumulates fast.

Run php artisan optimize to cache these:

php
// This command does the following:
// 1. Caches config files into storage/framework/config.php
// 2. Caches routes into storage/framework/routes.php
// 3. Caches event listeners into storage/framework/events.php
// 4. Caches service providers into storage/framework/providers.php

The output is a single compiled PHP file that Laravel loads instead of scanning directories. For an application with 50+ route files or 20+ config files, this cuts bootstrap time dramatically.

Always run this during your deployment process:

php
// In your deploy script (e.g., GitHub Actions, CircleCI)
php artisan config:cache
php artisan route:cache
php artisan event:cache

Or use the combined command:

php
php artisan optimize

Important: if you modify config or routes at runtime (which you rarely should), cached versions won't reflect those changes. Clear the cache before redeploying:

php
php artisan optimize:clear

In CI/CD pipelines, we clear and rebuild caches as part of the deployment steps, ensuring fresh production builds with zero stale cache entries.

Common mistakes

  • Caching mutable objects without cloning: If you cache an object and later modify it, the cached version changes too. Use clone or serialize to prevent this.
  • Forgetting to invalidate on updates: Cache a user's profile, then update it via an admin panel—the cache still returns old data. Always flush related tags when data changes.
  • Using short keys across multiple contexts: Naming your cache key 'users' works until you have users in multiple contexts (tenants, regions, environments). Use namespaced keys like 'tenant.5.users' instead.
  • Assuming Redis is always available: If your Redis instance goes down and your app crashes because cache lookups fail, you've misconfigured error handling. Always provide fallback logic.
  • Caching user-specific data without session handling: Storing a user's cart in cache without linking it to their session ID means they might see another user's cart. Always scope cache keys to user identity.

Key takeaways

  • Cache::remember() is your workhorse—it checks, executes on miss, and stores in one method call.
  • Redis is the production driver of choice for speed, introspection, and horizontal scaling.
  • Cache tags group related entries so you can invalidate multiple keys together without duplication.
  • Config and route caching via php artisan optimize reduce bootstrap overhead significantly; always rebuild on deploy.
  • Namespace your cache keys to avoid collisions across tenants, environments, or user contexts.
  • Monitor cache hit rates—if you're missing often, your TTL is too short or your key strategy is fragmented.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles