How would you scale a Laravel application?
Concept
Scaling a Laravel application — a senior developer needs to know the full scaling stack, from single-server optimization to horizontal scaling.
Level 1 — Single server optimization (do this first, before adding infrastructure):
- Enable OPcache with
validate_timestamps=0. php artisan optimize— caches config, routes, views.- Use Nginx + PHP-FPM (not Apache mod_php).
- Database query optimization: indexes, eager loading, query caching.
- Redis for cache and sessions.
- Queue slow operations (emails, notifications, exports).
Level 2 — Vertical scaling: More CPU/RAM on the same server. Cheap in the short term, hits a ceiling.
Level 3 — Read replicas: Move read queries to a replica. Laravel supports read/write connection arrays in config/database.php. Eloquent routes writes to write connection, reads to read connection automatically.
Level 4 — Caching:
- Query cache:
Cache::remember()for expensive queries. - HTTP cache:
Cache-Controlheaders for static pages. - Full-page cache: Nginx proxy cache or Varnish.
- Object cache: Cache model lookups (careful with invalidation).
Level 5 — Horizontal scaling (multiple app servers):
- Stateless app: sessions in Redis (not files), no local file state.
- Shared storage: S3 for user uploads (not local disk).
- Load balancer in front of multiple app servers.
- Sticky sessions or session sharing via Redis.
TrustProxiesmiddleware configured for the load balancer.
Level 6 — Queue workers: Separate server(s) for queue workers. Supervisor manages workers. Horizon for monitoring.
Level 7 — Microservices / separate services: Extract the bottleneck service. Usually premature before you've exhausted the above levels.
Code Example
<?php
// config/database.php — read/write split
'mysql' => [
'read' => ['host' => [env('DB_READ_HOST', '10.0.0.2')]], // replica
'write' => ['host' => [env('DB_WRITE_HOST', '10.0.0.1')]], // primary
'sticky' => true, // use write connection for remainder of request after a write
'driver' => 'mysql',
...
],
// Laravel automatically routes:
// SELECT queries → read connection
// INSERT/UPDATE/DELETE/raw writes → write connection// config/session.php — shared sessions for horizontal scaling
'driver' => 'redis', // not 'file' — files don't share across servers
// config/cache.php
'default' => 'redis',
// .env
SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=redis // or sqs for managed queues// Caching expensive queries
class ProductRepository
{
public function getFeatured(): Collection
{
return Cache::remember('products.featured', 3600, function () {
return Product::with('category')
->where('featured', true)
->orderBy('sort_order')
->get();
});
}
public function update(Product $product, array $data): Product
{
$product->update($data);
Cache::forget('products.featured'); // invalidate on change
return $product->fresh();
}
}; Supervisor config for queue workers — /etc/supervisor/conf.d/laravel.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600