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
whereIncarefully; large lists may benefit from temporary tables. - Leverage
chunkorcursorfor 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:
- Use Octane with Swoole/RoadRunner for the HTTP server.
- Cache database queries and views with Redis.
- Queue email, notifications, and heavy processing via Redis and Horizon.
- Add proper database indexes and eager load relationships.
- Use OPcache for PHP bytecode caching.
- 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.