A slow Laravel app costs you users, rankings, and money. The good news is that most performance problems come from the same handful of causes — uncached bootstrapping, blocking operations in the request cycle, and unoptimised queries. This guide covers the fixes I apply to every production Laravel project.
1. Caching Strategies
Config, route, and view caching
These three Artisan commands are the easiest wins and should be part of every production deployment script. They compile your config files, route definitions, and Blade templates into single cached files so Laravel doesn't re-parse them on every request:
php artisan config:cache
php artisan route:cache
php artisan view:cache
Run these after every deployment. If you change a config or route file locally, clear the cache with php artisan optimize:clear before testing.
Redis for data caching
Redis is the right choice for application-level caching in production — it's in-memory, extremely fast, and supports advanced features like cache tags. Set it as your cache driver and use Cache::remember() for expensive operations:
# .env
CACHE_DRIVER=redis
// Basic put/get
Cache::put('key', 'value', 600);
$value = Cache::get('key');
// Cache and compute in one call — the most common pattern
$users = Cache::remember('users.active', 3600, function () {
return User::where('active', true)->get();
});
Important: Always invalidate or update the cache when the underlying data changes. Stale cache is worse than no cache — users get wrong data and you'll spend hours debugging it.
2. Queue Optimization
Move blocking work off the request cycle
Any task that doesn't need to complete before you return a response should be a queued job — emails, notifications, report generation, image processing, third-party API calls. This is the single biggest improvement for perceived performance.
# Set queue driver
QUEUE_CONNECTION=redis
# Create a job
php artisan make:job SendWelcomeEmail
# Dispatch it
SendWelcomeEmail::dispatch($user);
# Run the worker
php artisan queue:work --tries=3
In production, use Supervisor to keep queue workers running and automatically restart them if they crash.
Laravel Horizon for monitoring
Horizon gives you a real-time dashboard for queue throughput, failed jobs, and worker status. It's essential once you're running queues in production:
composer require laravel/horizon
php artisan horizon:install
php artisan horizon
Access the dashboard at /horizon. Make sure to protect it with the gate in HorizonServiceProvider so it's not publicly accessible.
3. Scaling for High Traffic
Horizontal scaling
For high-traffic apps, you need to scale out rather than up. Run multiple Laravel app servers behind a load balancer (Nginx or AWS ELB). The key requirement is that your app must be stateless — no session or cache data stored on the local filesystem. Use Redis for both:
# .env — required for multi-server setup
SESSION_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
Database optimisation
Add indexes to every column used in WHERE, ORDER BY, and JOIN clauses. Fix N+1 queries with eager loading. For read-heavy applications, set up a read replica and point Laravel's read connection at it — this splits query load across two servers with almost no code changes.
# config/database.php — read/write split
'mysql' => [
'read' => ['host' => env('DB_READ_HOST')],
'write' => ['host' => env('DB_HOST')],
...
]
CDN for static assets
Serve images, CSS, and JS through a CDN like Cloudflare or AWS CloudFront. This reduces load on your origin server and dramatically improves load times for users far from your server location. Set your ASSET_URL in .env to point at your CDN.
4. Additional Best Practices
Runtime optimisations
Enable PHP OPcache on your server — it caches compiled PHP bytecode and is one of the highest-impact single changes you can make. Always run the latest stable PHP version. Use composer install --no-dev --optimize-autoloader in production to minimise the autoloader overhead.
Common mistakes to avoid
- Leaving
APP_DEBUG=truein production — exposes stack traces and kills performance - Using file-based sessions or cache in a multi-server setup — requests hit different servers and lose context
- Not running
config:cacheandroute:cacheafter deployment - Ignoring failed queue jobs — they pile up silently and can indicate bigger problems
- Skipping query profiling — use Laravel Debugbar or Telescope in development to catch slow queries early
Profiling tip: Don't optimise blindly. Use Debugbar or Telescope to measure first — you'll almost always find that 80% of the slowness comes from 2–3 queries or one blocking operation. Fix those before anything else.
Conclusion
Start with the quick wins — config/route/view caching and moving blocking tasks to queues. Then add Redis for data caching and fix your N+1 queries. By the time you need horizontal scaling, you'll have already eliminated most of the performance bottlenecks that would limit you anyway.
For more on database performance specifically, see the MySQL Query Optimization guide. For deployment infrastructure, see Deploying Laravel with Docker and Kubernetes.
Work with me
Need help with your Laravel architecture?
I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.