Sharding and partitioning

Concept 8 of 12 · 12 min read. Updated Oct 4, 2026.

Assumes you have read Back-of-the-envelope math.

When you finish this page you can
Say which resource forced the split, and why read replicas did not fix it.
Choose a partition key and name the query it makes expensive.
Explain how to add a shard to a live system without moving most of the data.

A primary that can't keep up#

Say you're building Instagram, and posts, likes and comments live in one Postgres primary with a read replica. In load tests the primary applies about 5,000 writes a second before its write-ahead log and disk fall behind. A normal day is 3,000 writes a second. Then a launch pushes it to 8,000.

Replicas copy every write, so they can't drain a write backlog.

Three things can run out on a single database, and only one of them forces a split:

What ran outCheaper fix to try first
Disk spaceA bigger volume, archiving or tiering old rows
Working set no longer fits in RAMMore memory, or a cache in front of the hot rows
Write throughputNone. This is the one that forces partitioning

There is also a cheap move between "one table" and "many machines": vertical partitioning, which splits columns instead of rows. If every page load reads a user's name and avatar but almost never their 2 KB bio, moving bio to its own table can shrink the hot table several times over. It buys time and adds no cross-machine queries.

Splitting rows across machines#

Sharding (horizontal partitioning) puts different rows on different machines. Every shard has the same schema and a slice of the rows. The column that decides which slice a row belongs to is the partition key.

The router, whether it is a library in your app or a proxy in front of the database, uses the partition key to pick a shard. A query that includes the key goes to exactly one shard. A query that doesn't has to ask all of them. Nearly every trade-off on this page comes back to those two sentences.

The key decides where today's writes land#

Range on a timestamp piles every new write onto one shard. Hashing a high-cardinality key spreads them.

Range partitioning by time still has a place. Time-range scans stay on one or two shards, and deleting a year of data is dropping a partition instead of deleting a billion rows. You accept a permanently hot write shard to get that.

Each common key makes some queries cheap and others expensive:

Partition keyStays on one shardGoes wrong when
user_id (hashed)Everything one user ownsOne user gets far more traffic than the rest
tenant_idEverything for one customer; per-customer deletes and exportsTenants differ in size by 1,000×
created_at (range)Time-range scans, archivingEvery new write is for "now"
Hash of the primary keyLookups by that key, very even writesAlmost any other query

Choose the key from the query you run most often. For Instagram that is usually "everything for this user", which points to user_id.

Even keys, uneven traffic#

Hashing user_id gives each shard about the same number of users. It does nothing about how much traffic each user brings.

One account with a large following turns its shard into the hot one, even though users are spread evenly.

What teams actually do about it:

  • Cache in front. The celebrity's profile and recent posts are the most cacheable rows you have. Most of those 40,000 reads should never reach the shard.
  • Extra read replicas for the hot shard only. Sharding and replication combine: each shard can have its own replicas, and the busy one can have more.
  • Handle the largest accounts on a separate path. Feeds commonly do this. Posts from accounts with millions of followers are merged in at read time instead of being copied to every follower when they are written. The news feed problem goes into this.

Uneven B2B tenants are the same problem with a different name: put your largest customers on dedicated shards on purpose, instead of hoping the hash spreads them out.

Queries without the key visit every shard#

A query that doesn't include the partition key becomes a scatter-gather. The router sends it to every shard, waits for every answer, and merges them.

A scatter-gather query is as slow as its slowest shard.

A scatter-gather query is only as fast as its slowest shard, and more shards make a slow one more likely. Suppose each shard has a 1% chance of a slow response at any moment (a GC pause, a compaction, a noisy neighbour):

Shards askedChance at least one is slow
11%
41 − 0.99⁴ ≈ 3.9%
321 − 0.99³² ≈ 27.5%
1001 − 0.99¹⁰⁰ ≈ 63.4%

Adding shards to scale writes makes every scatter-gather query slower at the tail. If a common read has to scatter, either the key is wrong or that read should be precomputed: a secondary index sharded by the other key, or a separate store kept up to date as writes happen.

Adding a shard later#

The obvious placement rule is shard = hash(key) % N. It spreads keys evenly and needs no lookup table. It falls apart the day you change N.

Same keys, same new shard. Modulo moved 40 of 48; the ring moved 6.

Consistent hashing places both shards and keys on a ring, and each key belongs to the next shard clockwise. A new shard takes over one arc, so only the keys in that arc move: about 1/(N+1) of them on average. In the clip D landed on a short arc and took only 6 keys, which shows the catch: with one position per shard the arcs come out uneven. Real implementations give each shard many positions (virtual nodes) so every shard owns many small arcs and the load evens out. This is the standard choice when nodes join and leave often, as in a distributed cache.

Most databases take a different route, logical shards. Create far more partitions than you have machines, say 1,024 buckets on 8 nodes. A key's bucket is hash(key) % 1024 and never changes. A small table maps each bucket to a node. Growing means moving whole buckets to a new node and editing that table.

Buckets move between machines. Keys never change bucket, so nothing is rehashed.

Moving a bucket while it is taking writes follows the same steps whichever scheme you use:

  1. Start writing to both the old and the new location.
  2. Copy the existing rows to the new location in the background.
  3. Compare the two copies until they match.
  4. Switch reads to the new location.
  5. Stop writing to the old location and delete it.

You can stop and roll back after any of those steps, which is what makes this safe to do on a live system.

Worked example: sharding Instagram's posts and likes#

Back to the hypothetical Instagram. Assume 50 million daily active users, each of whom creates 2 posts and 20 likes a day. Peak traffic is 3× the daily average. From the load test above, one primary handles 5,000 writes a second, and we plan to run each shard at 60% of that (3,000) to leave headroom.

Write rate

  • Posts: 50M × 2 = 100M a day. 100M ÷ 86,400 s ≈ 1,157 a second, or about 3,472 at peak.
  • Likes: 50M × 20 = 1B a day ≈ 11,574 a second, or about 34,722 at peak.
  • Peak total: about 38,194 writes a second.

Shard count

38,194 ÷ 3,000 ≈ 12.7, so 13 shards. Using 1,024 logical buckets, 10 machines hold 79 buckets and 3 hold 78. Going to 16 machines later means moving buckets and editing the table, with no rehashing.

Storage

Posts at about 1 KB each come to 100 GB a day. Likes at about 50 bytes each come to 50 GB a day. Together that is 150 GB a day, or roughly 55 TB a year: about 4.2 TB per shard per year across 13 shards.

Keys

Shard posts by user_id, because the profile page reads one user's posts. Shard likes by post_id, because the common read is "how many likes does this post have". Then check for hot shards: a celebrity's post can collect millions of likes, all on one post_id. Keep that count in a cached counter that is flushed periodically instead of running COUNT(*) against one shard.

How this comes up in an interview#

"Why not add more replicas?" Because the bottleneck is writes and every replica applies every write. Say this before you say "shard", together with the write rate you estimated.

"What's your partition key?" Name the key, the query it keeps on one shard, and one query it turns into a scatter-gather. Interviewers want to hear what you gave up as well as what you got.

"What happens when one user is huge?" Hot shard. Cache, more replicas for that shard, or a separate path for the biggest accounts.

"How do you add capacity next year?" Logical shards or consistent hashing, then the dual-write, backfill, verify, switch-over steps. Mentioning that % N moves most keys shows you have seen the problem before.

"How would you run a query across all users?" Avoid it on the request path. Precompute it, or accept a scatter-gather only for rare internal tools.

Next: keeping each shard alive#

Each shard is still one machine that can die. Making each one survive a failure means keeping copies on other machines, and once there are copies you have to decide what a reader sees right after a write. That is the next concept, replication and quorums.

Where this shows up

This page explains the mechanism on its own. Each problem below uses it under that problem’s own constraints.

Check yourself

Answer in your head first, then reveal
Disk is 80% full, the working set no longer fits in RAM, and writes are queueing on the primary. Which of these forces you to shard?
A B2B product shards by tenant_id. One customer is larger than the next 500 combined. What do you do?
Posts are sharded by user_id. Product wants a 'trending across everyone' page. What happens to that query, and what would you do instead?
You go from 4 shards to 8 with hash % N. What fraction of keys move?

Sign in to keep reading

The rest of HLD 101 is free with an account. Sign in once and every concept opens up.

That’s the last concept so far. Try a full problemDesign a URL ShortenerUses 7 of the 12 concepts above and needs nothing from sharding, replication or queues, so it works as a first problem.