Sharding: How Databases Scale Horizontally
Using a restaurant-chain analogy, this post explains how sharding splits data across multiple machines, compares common sharding strategies and their tradeoffs, and covers the problems that show up once you've sharded.
Picture a restaurant that's completely overbooked
Say you open a restaurant with just one big table, and a single server can handle every order, every dish, every check. Business picks up, the place fills to capacity, and that one server can't keep up anymore. The first instinct is usually "get a faster server" — upgrade the hardware. That's vertical scaling. But hardware has a ceiling; you can't keep piling on CPU and memory forever.
So you try something else: open more locations. Route customers to different branches based on some rule — where they live, say — and each branch only has to handle its own customers, without competing for the same resources. That's the core idea behind sharding: you split one huge dataset into smaller pieces, called shards, and spread them across multiple database servers, so no single machine has to hold all the data.
What problem sharding actually solves
Once a table gets big enough that a single machine's disk, memory, or CPU can't keep up, you'll eventually hit a physical ceiling no matter how much you upgrade — and the cost curve gets steep fast. Sharding lets you swap one absurdly expensive monster machine for a bunch of cheap ones instead. Reads and writes get spread across those machines and processed in parallel, which pushes up overall throughput.
It's worth being clear that sharding solves a different problem than replication. Replication makes multiple copies of the same data to boost availability and read throughput. Sharding splits data apart, so each shard holds a different slice, to break through a single machine's storage and compute limits. In practice, the two are often combined: each shard gets replicated on its own, so you get both scalability and fault tolerance.
Common sharding strategies
The rule that decides which shard a given row lands on is called the sharding key (or partition key). Which strategy you pick has a direct effect on performance and how painful maintenance is down the line.
Range-based sharding
Split data based on the range of some field's value — user IDs 1 through 1,000,000 go to shard A, 1,000,001 through 2,000,000 go to shard B, and so on.
// decide which shard to query based on the user_id range
function getShardByRange(userId) {
if (userId <= 1000000) return 'shardA'
if (userId <= 2000000) return 'shardB'
return 'shardC'
}The upside is that range queries — "find every user with an ID in this range" — are cheap, since the data is physically laid out in order. The downside is just as obvious: if data isn't generated evenly — say, new user IDs keep incrementing — the newest shard keeps taking all the traffic while the others sit idle. That's the hot spot problem.
Hash-based sharding
Run the sharding key through a hash function, and use the result to decide which shard it belongs on — typically by taking the hash modulo the number of shards.
// hash the key and mod by shard count to decide where data goes
function getShardByHash(userId, shardCount) {
const hash = simpleHash(String(userId))
return hash % shardCount
}Hashing spreads data out fairly evenly and sidesteps the hot spot problem. The tradeoff is that you lose ordering: range queries get painful, because consecutive IDs can end up scattered across completely different machines.
Worse, when you add or remove a shard, the modulus changes, and almost every piece of data gets reassigned — which basically means moving everything. That's why consistent hashing is the more common choice in practice: it maps the hash space onto a ring, with each shard owning a segment of that ring. Adding or removing a shard only reshuffles the small neighboring segment, not the whole system.
Directory-based sharding
Keep a separate lookup table that records which shard each key belongs to. On a query, you check that table first, then get routed to the right machine.
This is the most flexible option — you can redistribute data by whatever logic you want, and resharding just means updating the lookup table instead of a massive data migration. But the lookup table itself is now a new single point of bottleneck and failure: if it goes down, the whole system loses track of where anything is. That table usually needs its own high-availability setup.
The problems that show up once you've sharded
Sharding solves the single-machine bottleneck, but it turns things that used to be trivial on one database into cross-machine problems.
Cross-shard queries: if a query or a JOIN needs data spread across multiple shards, the database can't just handle it in one shot anymore — the application layer has to fetch from each shard separately and merge the results itself, which drives up both complexity and latency.
Distributed transactions: say a money transfer debits one user and credits another, and those two users happen to live on different shards. You can no longer lean on a single database's ACID transaction — you need a distributed transaction mechanism like two-phase commit, or you design the architecture up front to avoid needing strong cross-shard consistency in the first place.
Resharding: when a shard fills up again, you have to split it further, which means moving data between machines while keeping the service running the whole time. This is exactly why picking the right sharding key and strategy up front matters so much — get it right, and resharding later is a lot less painful.
Global unique IDs: the auto-incrementing ID that works fine on a single database breaks down once every shard is incrementing its own counter independently — they'll collide. You need something designed to generate globally unique values in a distributed setting, like UUIDs or Snowflake IDs.
So when do you actually need sharding
By now sharding might sound tempting, but it comes with real complexity — cross-shard queries, distributed transactions, the migration cost of resharding — and those are genuine engineering burdens, not hypothetical ones. The more practical order is usually: push vertical scaling, caching, and read replicas as far as they'll go first, and only reach for sharding once your data volume or throughput still can't keep up. Sharding is a powerful tool for scale problems, but its cost deserves real scrutiny — "we got bigger" isn't reason enough to reach for it on its own.
Wrapping up
At its core, sharding takes a dataset too big for one machine, breaks it into smaller shards, and spreads them across multiple machines — buying storage and throughput no single machine could reach on its own. Range-based sharding makes queries easy but is prone to hot spots. Hash-based sharding distributes data evenly but makes range queries and resharding hard. Directory-based sharding is the most flexible but adds a lookup table you now have to maintain. Whichever strategy you pick, sharding turns query consistency and transactions — things a single database used to just handle for you — into problems you now have to solve yourself at the application layer. That's the weight that belongs on the other side of the scale when you're deciding whether sharding is worth it.