Laravel Performance Optimization: Caching, Indexing, Queues, and Octane

Laravel Performance Optimization: Caching, Indexing, Queues, and Octane

Laravel is a powerful framework, but out-of-the-box performance can degrade under heavy load. This article dives into battle-tested techniques—caching, database indexing, eager loading, Redis, queues, and Octane—to keep your application fast.

1. Caching Strategies

Caching reduces redundant computations and database queries. Laravel provides a unified API for various drivers: file, database, Redis, Memcached, and DynamoDB.

Cache Facade vs. Cache Helper

Use the Cache facade or cache() helper. For example, caching expensive queries:

$users = Cache::remember('active_users', 3600, function () {
    return User::where('active', true)->get();
});

Tagged caching (Redis/Memcached) allows flushing related items:

Cache::tags(['users', 'orders'])->put('user_'.$id, $user, 3600);
Cache::tags('users')->flush();

Cache Drivers for Production

  • Redis: Best for most apps; persistent, fast, supports tags and atomic operations.
  • Memcached: Faster for simple key-value, no persistence.
  • File: Good for single-server dev, not for clustered production.

HTTP Caching with ETags

Use cache.headers middleware to set Cache-Control and ETag:

Route::get('/profile', function () {
    return response($profile)
        ->header('Cache-Control', 'public, max-age=3600')
        ->setEtag(md5($profile));
})->middleware('cache.headers:public;max_age=3600;etag');

2. Database Indexing

Proper indexing is critical. Laravel migrations allow index creation:

Schema::table('users', function (Blueprint $table) {
    $table->index('email');
    $table->unique('username');
    $table->index(['account_id', 'created_at']); // composite
});

Use EXPLAIN to analyze queries. For JSON columns, add indexes:

$table->index('options->language'); // MySQL virtual column

Query Optimization

  • Avoid SELECT *; fetch only needed columns: User::select('id', 'name').
  • Use whereIn carefully; large lists may benefit from temporary tables.
  • Leverage chunk or cursor for memory-efficient iteration.

3. Eager Loading

N+1 queries kill performance. Always eager load relationships:

// Bad: N+1
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name;
}

// Good: eager load
$posts = Post::with('author')->get();

// Nested: with('author.profile')
// Constrain: with(['comments' => fn ($q) => $q->where('approved', true)])

Use load for lazy eager loading when needed:

$posts = Post::all();
if ($someCondition) {
    $posts->load('comments');
}

Counting Related Models

Use withCount to avoid extra queries:

$posts = Post::withCount('comments')->get();
echo $post->comments_count;

4. Redis for Performance

Redis is more than a cache. Use it for sessions, queues, and real-time data.

Redis as Session Driver

Set SESSION_DRIVER=redis in .env. This offloads session storage from the database.

Rate Limiting with Redis

use Illuminate\Cache\RateLimiter;

$limiter = app(RateLimiter::class);
$key = 'login:'.$ip;
if ($limiter->tooManyAttempts($key, 5)) {
    abort(429);
}
$limiter->hit($key, 60); // 60 seconds decay

Locks for Critical Sections

$lock = Cache::lock('processing', 10);
if ($lock->get()) {
    // perform critical task
    $lock->release();
}

5. Queues and Job Processing

Offload slow tasks (email, image processing) to queues. Configure QUEUE_CONNECTION=redis for high throughput.

Job Example

namespace App\Jobs;

use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public User $user) {}

    public function handle(): void
    {
        // send email
    }
}

Queue Workers and Horizon

Use php artisan queue:work --queue=high,default for priority. For production, use Laravel Horizon to monitor and manage Redis queues:

composer require laravel/horizon
php artisan horizon:install
php artisan horizon

Horizon provides a dashboard, auto-scaling workers, and failure tracking.

Job Batching and Chaining

Bus::batch([
    new ProcessPodcast($podcast),
    new OptimizePodcast($podcast),
])->then(function () {
    // All jobs completed
})->dispatch();

6. Laravel Octane

Octane dramatically boosts performance by keeping the application in memory across requests. It uses Swoole or RoadRunner.

Installation and Configuration

composer require laravel/octane
php artisan octane:install
# Choose Swoole or RoadRunner

Start Octane:

php artisan octane:start --workers=4 --max-requests=500

Memory Leaks and State

Avoid class-level static properties that persist across requests. Use Octane::once() for shared state:

use Laravel\Octane\Facades\Octane;

$config = Octane::once('app.config', fn () => require config_path('app.php'));

RoadRunner vs Swoole

  • Swoole: More features (coroutines, websockets), but requires PHP extension.
  • RoadRunner: Written in Go, easier to debug, no extension needed.

Both can handle thousands of concurrent requests with minimal overhead.

7. Putting It All Together

A performant Laravel stack:

  1. Use Octane with Swoole/RoadRunner for the HTTP server.
  2. Cache database queries and views with Redis.
  3. Queue email, notifications, and heavy processing via Redis and Horizon.
  4. Add proper database indexes and eager load relationships.
  5. Use OPcache for PHP bytecode caching.
  6. Profile with Laravel Debugbar or Telescope, then optimize hot paths.

Key takeaway: Performance optimization is iterative. Measure first, then apply these techniques. Laravel's ecosystem gives you robust tools—use them wisely to build blazing-fast applications.