Back to Blog
·8 min read

Redis Caching in Laravel: A Practical Guide to Faster Applications

Laravel Redis PHP Performance Caching

Database queries are often one of the first performance bottlenecks in a growing Laravel application. A dashboard that loads hundreds of records, a product listing that repeatedly calculates prices, or an API endpoint that queries the same configuration on every request can put unnecessary pressure on your database.

Redis can help by keeping frequently accessed data in memory. Instead of running the same expensive query repeatedly, Laravel can return a cached result until it expires or the underlying data changes. The result can be lower database load and faster responses, provided the cache is designed correctly.

This guide explains how to configure Redis in Laravel, cache database queries, invalidate stale data, and diagnose common production issues.

What Is Redis, and Why Use It with Laravel?

Redis is an in-memory data store that supports several data structures and is commonly used for caching, queues, counters, and temporary application state.

Laravel provides a unified cache API, so application code can use Redis without directly managing Redis commands for every operation. You can also use Redis for queues, rate limiting, and other supported Laravel features.

Consider an API endpoint that returns a list of product categories. If the categories change infrequently but thousands of requests read them every hour, querying the database for every request may be unnecessary. A cache lets the application reuse the result for a defined period.

Step 1: Configure Redis in Laravel

First, ensure a Redis server is available and your PHP environment has the Redis client supported by your Laravel installation. The PHP Redis extension, commonly called phpredis, is one option. A compatible Composer client is another option where supported.

Set your cache configuration in the environment file:

REDIS_CLIENT=phpredis

CACHE_STORE=redis

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

These values are examples for a local Redis server. In Docker or Laravel Sail, the Redis hostname is often the service name, such as redis, rather than 127.0.0.1. In production, use the actual private endpoint, credentials, and TLS settings required by your Redis provider.

Do not expose Redis publicly or commit production credentials to your repository. Use environment variables or your hosting platform's secret-management system.

After changing environment configuration, clear cached Laravel configuration when appropriate:

php artisan config:clear

For production deployments that cache configuration, rebuild it as part of your deployment process:

php artisan config:cache

Check your installed Laravel version's configuration because environment variable names and defaults may differ between versions.

Step 2: Cache a Database Query

Laravel's Cache facade provides a simple way to cache the result of a database query. The remember method returns the cached value when available. Otherwise, it executes the callback, stores the result for the specified duration, and returns it.

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

public function index()
{
    $categories = Cache::remember(
        'product_categories:active',
        now()->addMinutes(30),
        fn () => ProductCategory::query()
            ->where('is_active', true)
            ->orderBy('name')
            ->get()
    );

    return response()->json($categories);
}

The cache key should describe the data being stored. The expiration period should reflect how frequently the data changes and how much staleness your application can tolerate.

For a query that depends on a tenant, user, locale, or filter, include those dimensions in the key. Otherwise, two requests with different inputs might accidentally reuse the same cached result.

$key = 'products:tenant:' . $tenantId
    . ':category:' . $categoryId;

$products = Cache::remember(
    $key,
    now()->addMinutes(10),
    fn () => Product::query()
        ->where('tenant_id', $tenantId)
        ->where('category_id', $categoryId)
        ->get()
);

For multi-tenant applications, tenant isolation must be enforced in the database query and, where appropriate, in the cache key. A cache key is not a substitute for authorization or tenant-scoped queries.

Step 3: Choose an Invalidation Strategy

Expiration alone is easy to implement, but it means users may see old data until the cache expires. If a product's price changes, you may need to invalidate the relevant cache immediately after the database update succeeds.

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

public function updatePrice(Product $product, float $price)
{
    $product->update(['price' => $price]);

    Cache::forget('product:' . $product->id);

    return $product->fresh();
}

In a real application, ensure the cache is invalidated after the database transaction commits successfully. If multiple endpoints cache the same product in different formats, invalidating only one key may leave other cached responses stale.

Cache tags

Some Laravel cache stores, including Redis, support cache tags. Tags can group related entries so they can be flushed together. Check your configured store and Laravel version before relying on this feature.

Cache::tags(['products', 'tenant:' . $tenantId])
    ->remember(
        'featured',
        now()->addMinutes(15),
        fn () => Product::query()
            ->where('tenant_id', $tenantId)
            ->where('is_featured', true)
            ->get()
    );

When products change, the matching tag group can be invalidated:

Cache::tags(['products', 'tenant:' . $tenantId])
    ->flush();

Use tag flushing carefully. A broad tag may invalidate many entries and cause a burst of database queries as traffic repopulates the cache.

Step 4: Avoid Cache Stampedes

A cache stampede happens when a popular cache entry expires while many requests arrive at the same time. Multiple requests may then run the same expensive database query simultaneously.

Possible strategies include refreshing popular entries before expiration, using a short lock around expensive cache population, or serving a slightly stale value while a background job refreshes it. The right approach depends on whether stale data is acceptable.

Laravel provides atomic locks when the configured cache driver supports them. A simplified pattern is:

$lock = Cache::lock('refresh:featured-products', 30);

if ($lock->get()) {
    try {
        $products = Product::query()
            ->where('is_featured', true)
            ->get();

        Cache::put(
            'featured_products',
            $products,
            now()->addMinutes(15)
        );
    } finally {
        $lock->release();
    }
}

This example illustrates the idea, but production code must handle lock acquisition failure, exceptions, and concurrent refresh attempts deliberately. Never assume that every request acquired the lock.

Step 5: Measure Whether Caching Helps

Do not add Redis caching to every query automatically. Frequently changing records, small indexed queries, and data used only once may not benefit enough to justify the added complexity.

Measure the application before and after the change. Useful metrics include:

  • API response latency, especially p95 and p99.
  • Database query count and query execution time.
  • Redis hit and miss rates.
  • Redis memory usage, evictions, and connection errors.
  • Application error rates and stale-data incidents.

Laravel query logging, application monitoring, and Redis metrics can help identify whether a slow endpoint is actually database-bound. Always test with representative traffic rather than relying on a single local request.

Common Redis Caching Problems

Redis connection refused

Check that Redis is running and that Laravel is using the correct host and port. When Laravel runs inside a container, 127.0.0.1 points to that container, not necessarily to the Redis container.

Data does not update after a database change

Check whether every relevant cache key is invalidated. Review model events, service methods, queued jobs, and transaction boundaries. Also verify that all application instances use the expected cache store and key prefix.

Cache works locally but not in production

Compare environment configuration, network access, credentials, TLS settings, and Redis permissions. After changing environment variables, ensure cached Laravel configuration has been rebuilt where necessary.

Memory usage keeps growing

Review key expiration, cache key cardinality, large serialized values, and the Redis eviction policy. Avoid generating a unique key for every request unless there is a clear need and an expiration strategy.

Redis Cache vs. Database: What Should You Cache?

Good candidates include configuration, category lists, popular product listings, expensive aggregates, and data that is read frequently but changes relatively infrequently.

Be more careful with payment status, inventory, account balances, and other correctness-sensitive data. A cache may be useful for display or read optimization, but it should not become the authoritative source for critical business decisions without a carefully designed consistency model.

Final Thoughts

Redis can significantly improve a Laravel application's performance when it removes repeated expensive work. The most important part is not adding a cache call; it is choosing the right data, defining an expiration and invalidation strategy, protecting concurrent refreshes, and measuring the result.

Start with one slow endpoint, collect baseline metrics, introduce caching, and compare the results under realistic traffic. That approach makes performance improvements measurable and helps avoid stale data or unnecessary operational complexity.