Scaling strategies
Horizontal vs vertical, stateless design, connection pooling.
"Vertical or horizontal" is shorthand for a deeper choice: do I throw a bigger machine at the problem, or change the design to share it across many small ones? The deeper choice shapes every other piece of your architecture.
The big idea
Two axes:
| Attribute | Vertical (scale up) | Horizontal (scale out) |
|---|---|---|
| Action | Bigger box: more CPU, RAM, disk | More boxes; identical instances behind a load balancer |
| Code change | None | Stateless services; shared session store |
| Limit | The biggest box exists | Almost none for stateless tiers |
| Cost | Cheap until it isn't | Linear-ish in compute |
| Failure model | One machine = all your traffic | Lose one, the others keep running |
For nearly every modern app: vertical for the database, horizontal for everything else.
Make services stateless
A stateless service stores no per-user state in memory between requests. Every request can be routed to any instance. That's the magic that makes horizontal scaling work.
Store session data in a shared cache (Redis), database, or signed cookie.
const session = await redis.get(`session:${sid}`);
if (!session) return res.status(401).end();session = await redis.get(f"session:{sid}")
if session is None:
return Response(status_code=401)Keep user objects in a module-level Map on each instance.
const sessions: Map<string, Session> = new Map(); // dies with the process
sessions.set(sid, session);sessions: dict[str, Session] = {} # dies with the process
sessions[sid] = sessionConnection pooling
Each app instance opens DB connections; if 50 instances each open 100 connections, that's 5,000 connections — far more than Postgres can handle. Pool inside each instance, and use a connection pooler (PgBouncer, RDS Proxy) in front of the database.
Scaling the database
Stateful tiers are harder. The escalation:
Vertical first
Bigger instance. Postgres on a 64-core box goes a long way.
Read replicas
Reads go to replicas; writes still hit the primary. Watch for replication lag bugs.
Sharding / partitioning
Split data across multiple primaries by some key (user_id, tenant_id). Now you have a distributed system; the rules change.
Specialised stores
Move hot workloads off: ElasticSearch for search, Redis for sessions, Kafka for streams, ClickHouse for analytics.
Async over sync where you can
Synchronous work limits throughput to the latency of the slowest dependency. Pushing slow work to a queue/worker frees the request path:
Order placed → return immediately, send the receipt email asynchronously. The user sees "order confirmed" in milliseconds; the email is queued for a worker.
Watch for hidden bottlenecks
Even a stateless horizontal service still funnels through:
- The database
- Eventually you hit max connections or write throughput.
- Caches
- Hot keys serialise on a single Redis instance.
- Locks
- Anywhere
SELECT ... FOR UPDATEhappens, contention kills throughput. - Third-party APIs
- Their rate limit is your scaling ceiling.
In practice
Most teams scale prematurely. The right order:
- Profile the slow path.
- Add an index, cache the obvious answer, batch the N+1 queries.
- Add app instances (cheap, easy).
- Then consider read replicas, queues, sharding.
By the time you're sharding the database, you should know the system intimately. If you don't, sharding will make things worse.
Key takeaways
- Vertical for the DB, horizontal for everything else.
- Stateless services are a precondition for horizontal scale.
- Connection pooling — both in-app and in front of the DB — keeps Postgres alive.
- Push slow work to queues + workers; return fast.
- Profile, index, cache before you shard. Most scaling work is engineering, not infra.
Checkpoint questions
Use these to test whether the lesson is clear enough to explain without rereading.
- 1Which parts of a system are easiest to scale horizontally, and which are hardest?
- 2Why does stateless service design make load balancing simpler?
- 3What hidden bottlenecks can remain after adding more application servers?
- 4When should you scale the database vertically before adding replicas or shards?
References
External resources for going deeper after the lesson above.