Vertical scaling: make the machine bigger
Add more CPU, RAM, or faster disks to a single server. This is the simplest scaling strategy:
- No code changes
- No distributed systems complexity
- No load balancer needed
- Works until you hit the physical limits of the largest available machine
For a PostgreSQL database, vertical scaling is often the right first move. Going from 4 cores to 16 cores can 4x your query throughput with zero application changes.
Horizontal scaling: add more machines
Run multiple instances of your application behind a load balancer. Each instance handles a fraction of the traffic.
┌── Instance 1
Load Balancer ────┼── Instance 2
└── Instance 3
Horizontal scaling requires:
- Stateless application: no in-memory sessions, no local file storage
- External session store: Redis or database-backed sessions
- Load balancer: distributes traffic across instances
- Shared storage: S3 or NFS for file uploads
When to scale vertically
- Database: until you need read replicas or sharding, a bigger database server is simpler
- Single-threaded workloads: Python, Ruby, or single-process Node.js benefits from faster CPUs, not more instances
- Prototyping: when you need more headroom fast and do not want to redesign
- Cost: below a certain scale, one large server is cheaper than three small ones plus a load balancer
When to scale horizontally
- Stateless web servers: Node.js APIs, Next.js apps — these are designed for horizontal scaling
- High availability: if one instance dies, the others keep serving traffic
- Auto-scaling: add instances during peak hours, remove them at night
- Beyond single-machine limits: when the biggest available server is not enough
The practical playbook
Step 1: Optimize before scaling
Before adding hardware, check if your code is the bottleneck:
-- Are you missing indexes?
SELECT * FROM pg_stat_user_tables WHERE seq_scan > 10000;
-- Are queries slow?
SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
I have seen teams scale from 2 to 8 servers when the real problem was a missing database index.
Step 2: Scale vertically until it hurts
Doubling your server from 4GB/2CPU to 8GB/4CPU is a 5-minute change. Going from 1 instance to 3 requires a load balancer, health checks, shared sessions, and deployment pipeline changes.
Step 3: Scale horizontally when you need resilience
The inflection point is usually availability, not performance. When a single server going down means your app is down, that is when horizontal scaling becomes mandatory — not because you need the capacity, but because you need the redundancy.
Scaling the database
Databases are the hardest part to scale horizontally:
- Read replicas: route reads to replicas, writes to the primary. Works for read-heavy workloads.
- Connection pooling: PgBouncer lets more app instances share fewer database connections.
- Sharding: split data across multiple database servers by a shard key. Last resort — adds enormous complexity.
App Instances ──→ PgBouncer ──→ Primary (writes)
──→ Replica 1 (reads)
──→ Replica 2 (reads)
My rule of thumb
Scale the app horizontally. Scale the database vertically. Add read replicas when the primary's CPU is consistently above 60 percent. Shard only when you have exhausted every other option.
