Scaling from One User to Millions
Scale in the cheapest order — indexes, connection pool, read replicas, cache, partitioning, sharding — because each step buys capacity by adding a problem, and skipping to sharding is adding the biggest problem first.
The ladder
One box serves the first thousand users. The first wall is almost always a missing index, not a missing server — one composite index can be a 100× win that no hardware matches. Next, a connection pool: each Postgres connection is a process, so twenty app instances opening fifty connections each is a dead server, and a pool of twenty reused connections serves far more. Next, read replicas: most workloads are 90%+ reads, and replicas add read capacity almost linearly plus a failover target — at the cost of replication lag. Next, a cache for the hot, repeated reads — at the cost of invalidation. Next, partitioning to keep a huge table operable. Finally sharding, the only step past a single machine’s write ceiling, and by far the most expensive.
The order matters because each rung is cheaper and less invasive than the next, and each solves a problem the previous rung created. Teams that jump straight to sharding or microservice-per-database are adding the hardest problem before exhausting the easy ones — and usually before they have the traffic to justify any of it.
Vertical scaling is underrated
Before any of the distributed rungs, buy a bigger machine. A modern server has dozens of cores and hundreds of gigabytes of RAM; a working set that fits in RAM runs from memory. Doubling the instance is a config change and a restart, with no new failure modes, no consistency questions, and no application changes. It runs out eventually, but "eventually" is further away than most teams assume, and every rung you postpone is a class of bug you do not yet own.
What each rung costs
Indexes cost write speed and storage. A pool costs one component and breaks session state in transaction mode. Replicas cost money per replica and hand you replication lag — the "I saved it and it shows the old value" bug; see Replication and Read Scaling. A cache costs a whole class of staleness bug and a component whose loss must be survivable; see Caching Patterns. Partitioning costs schema complexity and a maintenance job; see Partitioning and Sharding. Sharding costs joins, cross-shard transactions, global uniqueness, and rebalancing — the largest step change on the ladder.
The discipline: at each rung, measure that the previous one is genuinely exhausted before climbing. "The database is slow" is not a reason to add a replica if the real cause is one unindexed query.
Key points
- Indexes → pool → replicas → cache → partition → shard. Cheapest and least invasive first.
- The first wall is usually a missing index, not missing hardware.
- Vertical scaling buys a lot with no new failure modes; use it before distributing.
- Each rung buys capacity by adding a problem; climb only when the previous rung is measurably exhausted.
From one user to millions
Nothing. One application, one database, one machine.
A single Postgres instance on modest hardware serves thousands of users. Most systems never truthfully need more than this plus indexes.
One instance. Backups are a cron job.
- Get the schema and the indexes right. Everything below is harder if these are wrong.
When to use — and when not
- Any growing system, at the point a specific metric shows a specific limit.
- Pre-emptively, on projected traffic. Build for 10× current, not 1000×.
Failure modes
- Sharding before indexing.
- Adding replicas to fix a problem one index would solve.
- A cache added without an invalidation plan.
See how this works internally →
Descend one layer: the same topic explained from the machinery up.