Horizontal scaling vs vertical scaling — more machines vs bigger machine
Concept
Horizontal vs vertical scaling — two strategies for handling more traffic or load.
Vertical scaling (scale up): Give the existing server more resources — bigger CPU, more RAM, faster disk. Simple: no application changes required. But limited: there's a maximum server size. Also a single point of failure.
Horizontal scaling (scale out): Add more servers and distribute the load between them. Theoretically unlimited. Requires a load balancer to distribute requests. Stateless applications scale horizontally with no issues. Stateful applications (session in memory) require shared session storage (database or Redis).
Which to use:
- Vertical: Simple apps, early stage, single database, when you need to scale quickly with minimal architectural change.
- Horizontal: High availability requirements, traffic spikes, unlimited scale potential, microservices.
PHP and horizontal scaling:
- PHP-FPM is stateless per request — naturally horizontally scalable.
- Sessions must be in Redis/database, not file (shared across servers).
- File uploads must go to shared storage (S3), not local disk.
- Caches must be in Redis, not in-memory (each server would have different cache).
Database scaling: Usually vertical first (more powerful DB server). Read replicas for horizontal read scaling. Sharding for write scaling.
Auto-scaling: Cloud platforms (AWS, GCP, Azure) can automatically add/remove servers based on CPU or request volume. Laravel Vapor (serverless) handles horizontal scaling automatically.
Code Example
<?php
// HORIZONTAL SCALING REQUIREMENTS
// BAD: Session in file (each server has different sessions)
// config/session.php
'driver' => 'file', // ← won't work across multiple servers
// GOOD: Session in Redis (shared across all servers)
'driver' => 'redis',
'connection' => 'default',
// BAD: Cache in file/array (different cache per server)
// config/cache.php
'default' => 'file', // ← each server has different cache state
// GOOD: Shared cache in Redis
'default' => 'redis',
// BAD: Storing uploads on local disk
public function upload(Request $request): void
{
$request->file('avatar')->store('avatars'); // local disk
// Server 2 can't access Server 1's local disk!
}
// GOOD: Uploads to shared storage
public function upload(Request $request): void
{
$request->file('avatar')->store('avatars', 's3'); // shared S3 storage
// All servers read from the same S3 bucket
}
// Queue workers scale horizontally too
// Instead of one queue:work process, run many workers across many servers
// Each worker picks up the next available job from the shared Redis queue# docker-compose.yml — horizontal scaling with replicas
services:
app:
image: myapp:latest
deploy:
replicas: 3 # 3 instances of the app
environment:
CACHE_DRIVER: redis
SESSION_DRIVER: redis
FILESYSTEM_DISK: s3
load_balancer:
image: nginx
ports:
- "80:80"
depends_on:
- app
# Distributes traffic across the 3 app replicas# AWS: Vertical scaling (resize EC2 instance)
# t3.micro (2 vCPU, 1GB RAM) → c6i.xlarge (4 vCPU, 8GB RAM)
# Requires stop/start of instance — downtime!
# AWS: Horizontal scaling (Auto Scaling Group)
# Sets min=2, max=10 instances
# Adds instances when CPU > 70%, removes when CPU < 30%
# Zero downtime — requests just go to remaining instances