0

How would you scale a Laravel application?

Advanced5 min read·eng-10-004
interviewperformance

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-Control headers 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.
  • TrustProxies middleware 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
<?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
php
// 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
php
// 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();
    }
}
ini
; 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