By 2021 Notion's single Postgres database had reached the point where VACUUM stalled and transaction ID wraparound became a genuine risk. They sharded Postgres into 480 logical shards rather than moving block data to a NoSQL store. What forced the change, what decided the destination, and where would copying it be a mistake?
Show the full answer Hide the answer
The situation they were in
Notion's data model is blocks: every paragraph, page and database row is a block, and blocks reference each other. By 2021 the monolithic Postgres instance had grown to the point where, as their engineering blog describes, autovacuum stopped keeping up and could not reclaim dead tuples. That is not a performance annoyance. Postgres assigns 32-bit transaction IDs, and if vacuuming falls far enough behind, the database stops accepting writes to avoid wraparound corruption. The clock was on a hard limit, not a latency target.
What they chose
480 logical shards spread across 32 physical Postgres machines, with routing done in the application by the workspace the block belongs to. The number 480 was picked for its factors: 480 splits evenly across 32, 40, 48, 60, 80 and 96 hosts, so growing the physical fleet never requires re-splitting the data again — only moving whole logical shards. They took exactly that path in 2023, moving to 96 physical machines while keeping 480 logical shards.
The cutover ran as double-write to old and new, backfill of history, verification, then switch-over — the standard sequence, with the verification step carrying most of the risk.
Why it fit their constraints
Their access pattern is almost always scoped to one workspace. That makes a natural partition key, and it means the queries that matter never need to cross shards. Once the partition key is obvious and the queries are local, the argument for a different data model largely evaporates: what they needed was more machines, not different semantics. Staying on Postgres kept transactions, joins within a workspace, the operational knowledge the team already had, and years of query tuning.
What it cost them
Cross-shard work became the application's problem — aggregate queries, analytics and anything not keyed by workspace now need fan-out or a separate system; Notion later published its work on a data lake fed by change capture, which takes analytical load off the sharded transactional store. Schema migrations now run 480 times. Connection counts multiply by the number of physical hosts. The routing logic is application code that every new service must get right, and the cost of a bug in it is reading another tenant's data.
When copying this would be the wrong answer
Three conditions did the work here, and a team missing any of them should not copy this:
- A partition key that covers nearly all queries. Without one, sharding relocates the problem into fan-out.
- An urgent structural limit, not a slow database. Wraparound risk is a deadline; p99 of 300 ms is a tuning exercise. Most teams that reach for sharding are read-bound, and replicas plus indexes are the cheaper answer.
- The staff to run 32 databases, then 96.
The generalisable lesson is not "shard Postgres". It is choose the logical shard count once, high, with many factors — the reversibility that the 2023 move depended on was designed in two years earlier.