Skip to content
All posts
LaravelRedisDatabase

Your Laravel App Is Slow Because You’re Hitting the Database Too Much. Redis Fixes That.

March 8, 2026·Read on Medium·

A practical guide to caching, queues and sessions with Redis in Laravel. No hand-waving. Actual implementation.

Most Laravel performance problems are not framework problems. They’re not even PHP problems. They’re architecture problems. The most common one is simple: you’re asking the database to do work it’s already done, over and over, for every request.

A user loads a dashboard. Your app runs 12 queries to build it. The data hasn’t changed since the last request. Another user loads the same dashboard. Same 12 queries. Same results. Another request, same queries. You’re treating your database like a library that throws away every book after you return it and reprints it from scratch next time.

Redis stops that. Not by magic, but by doing what it does well: storing data in memory where it can be retrieved in microseconds instead of milliseconds, surviving across requests, supporting data structures that make caching patterns clean and handling concurrent access without the locking problems you’d get from file-based caching.

This article walks through the actual implementation, not just the theory.

What Redis Is and Why It’s Different

Redis stands for Remote Dictionary Server. It’s an open-source, in-memory data store that operates as a key-value store but with support for richer data structures: strings, hashes, lists, sets, sorted sets, bitmaps, streams. Because it stores data in RAM rather than on disk, reads and writes are orders of magnitude faster than a traditional relational database.

Laravel supports multiple cache drivers out of the box: file, database, Memcached, Redis and DynamoDB. The file driver is fine for development. The database driver is acceptable for low-traffic applications. Redis is the right choice for anything that needs real performance under load.

Benchmarks comparing Laravel cache drivers consistently show Redis significantly outperforming file-based caching. One real-world comparison found response times dropping from around 480ms to 160ms after introducing Redis caching for frequently accessed queries , roughly a 66% improvement. Another found database-driver cache to be approximately five times slower than Redis for typical operations.

Redis also does things the file driver simply cannot: it supports cache tags (letting you invalidate groups of related cached items at once), pub/sub messaging for real-time features, sorted sets for leaderboards and rate limiting and atomic operations that prevent race conditions in concurrent environments.

Laravel Sail includes Redis by default, which tells you something about where the framework community has landed on this question.

Installation

You need two things: Redis running on your server and a PHP Redis client in your project.

Installing Redis on Ubuntu:

sudo apt update
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server

Verify it’s running:

redis-cli ping
# Returns: PONG

PHP Client: predis vs phpredis

Laravel supports two PHP Redis clients. predis/predis is a pure PHP implementation installed via Composer, easier to set up and what most projects use. phpredis is a C extension that's faster but requires enabling at the PHP level.

For most projects, predis is the right choice:

composer require predis/predis

Laravel configuration:

In your .env file:

CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CLIENT=predis

That’s it for setup. Laravel’s cache abstraction handles the rest.

The Four Caching Patterns You’ll Actually Use

Laravel’s Cache facade gives you a clean API regardless of which driver is behind it. These are the patterns worth understanding.

Pattern 1: Remember (the one you’ll use most)

$users = Cache::remember('users.all', 3600, function () {
    return User::with('profile')->get();
});

remember checks whether the key exists in Redis. If it does, it returns it immediately. If not, it runs the closure, stores the result with the given TTL (in seconds) and returns it. One line replaces what would otherwise be a manual check-store-return pattern.

Pattern 2: rememberForever (for data that doesn’t change)

$countries = Cache::rememberForever('countries', function () {
    return Country::orderBy('name')->get();
});

Use this for reference data: country lists, currency codes, configuration values that change only on deploys. You’ll invalidate these manually when the underlying data changes.

Pattern 3: Tags (for invalidating related items)

// Store with a tag
Cache::tags(['users'])->put("user.{$userId}", $userData, 3600);
// Invalidate all tagged items at once
Cache::tags(['users'])->flush();

Cache tags let you group related cached items and flush them together. When a user’s profile is updated, you can flush everything tagged with their user context. Note that tags are only supported by Redis and Memcached, not the file or database drivers.

Pattern 4: Atomic remember (preventing the cache stampede)

Under high traffic, if multiple requests arrive simultaneously for an expired key, they’ll all miss the cache at once and all hit the database. This is called a cache stampede.

$data = Cache::flexible('expensive.data', [60, 120], function () {
    return $this->runExpensiveQuery();
});

Laravel’s flexible method (introduced in Laravel 11) handles this by serving stale data while refreshing in the background. It's the right pattern for high-traffic endpoints with expensive queries.

Caching Database Queries in Practice

Here’s a realistic controller method without caching:

public function dashboard(User $user)
{
    $stats = [
        'total_orders'    => Order::where('user_id', $user->id)->count(),
        'pending_orders'  => Order::where('user_id', $user->id)->where('status', 'pending')->count(),
        'total_spent'     => Order::where('user_id', $user->id)->sum('total'),
        'recent_activity' => Activity::where('user_id', $user->id)->latest()->take(10)->get(),
    ];
    return view('dashboard', compact('stats'));
}

Four database queries on every page load, every time. Here’s the same method with Redis caching:

public function dashboard(User $user)
{
    $stats = Cache::remember("dashboard.{$user->id}", 300, function () use ($user) {
        return [
            'total_orders'    => Order::where('user_id', $user->id)->count(),
            'pending_orders'  => Order::where('user_id', $user->id)->where('status', 'pending')->count(),
            'total_spent'     => Order::where('user_id', $user->id)->sum('total'),
            'recent_activity' => Activity::where('user_id', $user->id)->latest()->take(10)->get(),
        ];
    });
    return view('dashboard', compact('stats'));
}

Now those four queries run once per user per five minutes regardless of how many times the dashboard is loaded. The first visitor after cache expiry pays the database cost. Everyone else gets the cached response in microseconds.

When a user places a new order, you invalidate their specific cache:

Cache::forget("dashboard.{$user->id}");

Using Redis for Queues

Caching is half of what Redis does well in Laravel. Queues are the other half.

Offloading time-consuming operations to a background queue is one of the most impactful performance changes you can make to a Laravel application. Sending email, generating PDFs, processing images, calling slow external APIs. None of these should happen in the request cycle.

// Instead of sending email in the request:
Mail::to($user)->send(new WelcomeEmail($user));
// Dispatch it to a queue:
dispatch(new SendWelcomeEmail($user));

The queue configuration in .env:

QUEUE_CONNECTION=redis

Start a queue worker:

php artisan queue:work redis --sleep=3 --tries=3

In production, use a process manager like Supervisor to keep your queue workers running. Laravel Forge and Laravel Cloud handle this for you automatically.

Laravel Horizon gives you a dashboard to monitor your Redis queues. Install it, configure it and you get real-time visibility into queue throughput, failed jobs and worker status. It’s the right tool for any production application using Redis-backed queues.

Using Redis for Sessions

Redis also works as a session driver. The advantage over file-based sessions is meaningful if you’re running multiple application servers: Redis sessions are shared across all servers, so a user who hits server 1 for one request and server 2 for the next request stays logged in.

SESSION_DRIVER=redis

No code changes required. Laravel handles the rest.

Rate Limiting with Redis

Laravel’s rate limiting uses Redis when you configure it to do so and it’s substantially better than the database-backed alternative for this use case:

// In a service provider or route definition:
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

In config/route.php, set the throttle middleware to use Redis when your cache driver is Redis. Laravel's documentation is explicit that Redis rate limiting is preferred over the database option for performance on high-traffic routes.

Redis Pub/Sub for Real-Time Features

If you’re using Laravel Reverb, the first-party WebSocket server, Redis becomes even more central to your application. Reverb can use Redis pub/sub to broadcast events to WebSocket connections. You publish an event in one process, Redis distributes it to all connected clients through Reverb.

broadcast(new OrderStatusUpdated($order))->toOthers();

That single line, with the right configuration, pushes a real-time update to every browser tab watching that order’s status. No polling. No manual WebSocket management. The plumbing runs through Redis.

Memory Configuration: Don’t Skip This

Redis is fast but it’s not infinite. Running Redis without a memory limit in production means it will consume as much RAM as it needs until your server runs out. Configure a limit and an eviction policy.

In redis.conf or your Redis configuration:

maxmemory 512mb
maxmemory-policy allkeys-lru

allkeys-lru evicts the least recently used keys first when memory is full. This is the right policy for a cache: old, stale data gets evicted to make room for new data. For a typical Laravel application with moderate traffic, start with 512 MB to 2 GB depending on your cached data volume and monitor from there.

Laravel Horizon includes memory monitoring. Redis Insight is a GUI tool from Redis Labs that gives you detailed visibility into key distribution, memory usage and latency. Both are worth using in production.

What Not to Cache

A quick note on this because it’s easy to overcache and create subtle bugs.

Don’t cache data that changes often and where serving stale results has real consequences. User account balances, inventory counts, anything that drives a business decision that the user expects to be current. The five-minute TTL that works fine for a dashboard stat is wrong for a checkout page showing stock levels.

Don’t cache data per-request if the operation is fast. Caching a query that takes 2ms with a key that takes 1ms to resolve is adding complexity for minimal gain. Profile first, cache second.

Don’t cache sensitive data without thinking about the implications. A shared cache key with user data in it that doesn’t include a user identifier can accidentally serve one user’s data to another.

The Performance Numbers Are Real

Redis caching changes the performance profile of a Laravel application in measurable ways. Latency drops from the hundreds-of-milliseconds range for uncached database reads to single-digit milliseconds for cache hits. Under high concurrency, the reduction in database load means queries that do need to run against the database get more resources and complete faster.

The infrastructure cost argument is also real. Fewer database queries means you can handle more traffic with less database compute. Redis memory is cheap compared to database instance costs. Applications with good caching strategies consistently handle more traffic for less money.

The setup time to install Redis and implement basic Cache::remember on your most-hit queries is a few hours. The performance return on that investment will show up immediately in your response time metrics.

If you’re building a new Laravel application in 2026, start with Redis configured from day one. If you’re inheriting one that doesn’t use it, start with your most expensive queries and work outward from there.

The database doesn’t need to do the same work twice.

Redis is available as a managed service from Upstash (serverless, free tier available), Redis Cloud and DigitalOcean Managed Redis. Laravel Cloud and Laravel Forge both support Redis configuration directly in their respective dashboards.

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium
Your Laravel App Is Slow Because You’re Hitting the Database Too Much. Redis Fixes That. — Hafiq Iqmal — Hafiq Iqmal