Notion didn't shard Postgres because it ran out of space. It sharded because a counter was quietly ticking toward a number that would have frozen every write in the company.
Five years of growth on a single Postgres monolith. Two problems converged: the cleanup process (VACUUM) couldn't keep up, and a 32-bit transaction-ID counter was creeping toward wraparound — a limit that, if hit, forces Postgres to stop accepting writes to protect your data. Not slow. Stopped.
Every change Postgres makes gets a transaction ID — a ticket number that only counts up. The counter is 32 bits wide, so it eventually runs out of numbers and must wrap around to reuse old ones. To do that safely, a background process (VACUUM) has to "freeze" old rows first. If writes outrun VACUUM, the counter approaches the danger zone — and Postgres slams the brakes on all writes to avoid corrupting data.
So Notion wasn't fighting disk space. They were fighting time. The single database had a structural ceiling, and they could see the date approaching.
One database became 480 shards
Notion sharded the monolith by workspace ID — every workspace's data lives entirely on one shard, which keeps the queries that matter (everything inside your workspace) single-shard. The structure has two layers: 32 physical databases, each holding 15 logical shards. That's 480 logical shards in total.
The two-layer trick — logical shards mapped onto physical machines — is the same instinct Figma reached for independently. The first time you shard, you'll want to do it once. Decoupling logical from physical means the next rebalance is a config change, not another migration.
Sharding a live database that the entire product depends on, with zero downtime, is the actual engineering. You can't take Notion offline to copy data. So the cutover runs as a sequence of overlapping, reversible phases.
Why dark reads are the part that matters
Double-writing and backfilling get the data into the new shards. But how do you know the sharded system returns the same answers? You don't trust it — you verify. In the dark reads phase, production reads hit both the old monolith and the new shards, and the results are compared. Mismatches are bugs, surfaced before a single user depends on the new path. Only when they match consistently do you cut over.
Worth knowing
The choice of workspace ID as the shard key is doing quiet, heavy lifting. Notion's access pattern is overwhelmingly "give me everything in this workspace." Co-locating a workspace's data on one shard means the common query never crosses shard boundaries. Pick a key that matches your dominant access pattern, and cross-shard queries stay rare. Pick wrong, and every page load becomes a scatter-gather.
The gap it reveals
"Postgres can't scale, so shard it" is surface-level. The real understanding is knowing why a single Postgres hits a wall — VACUUM throughput and transaction-ID wraparound, not raw size — and that the dangerous, skill-defining part of sharding is the live, zero-downtime cutover, not the data split. The dark-reads phase is the kind of detail you only know if you've actually done a migration that couldn't fail.
In the interview room
"How would you migrate a live database with no downtime?" is a top-tier system-design question precisely because the naive answer ("take a backup and restore it") reveals you've never done it. Walk the interviewer through double-write → backfill → reconcile → dark-read verify → flip, and emphasise that every step is reversible. That sequence is the senior-engineer fingerprint.
The reframe
Notion's hardest problem wasn't designing the sharded layout — it was changing the engine of a moving car without the passengers noticing. The scaling limits forced the migration; the caution of the migration is what made it succeed. Reversibility at every step, and verification before trust, are not slowdowns. They're the entire discipline.
The schema redesign is the easy half. Getting there without breaking is the job.
Primary source →
notion.com — Herding Elephants: Lessons Learned from Sharding Postgres at Notion