Database Sharding Strategies: Consistent Hashing, Rendezvous Hashing and Online Resharding (2026)

Database Sharding Strategies: Consistent Hashing, Rendezvous Hashing and Online Resharding (2026)

Database Sharding Strategies: Consistent Hashing, Rendezvous Hashing and Online Resharding (2026)

Every sharded system makes one decision on day one that it will spend years paying for: how a key becomes a shard number. Pick hash(key) % N and the first time you add a node, roughly nine out of ten rows have to move. Pick a shard key with a skewed distribution and one machine melts while the other nine idle. Skip the resharding story and “we will just add capacity later” becomes a quarter-long migration with a pager attached.

Database sharding is the practice of splitting one logical dataset across many independent database instances so that storage, write throughput and failure domains scale horizontally. This guide covers the three placement families (range, hash and directory), the algorithms that make hash placement stable under membership change (consistent hashing with virtual nodes, jump hash and rendezvous hashing), how to choose a shard key, why cross-shard queries and transactions are the real cost, and how to reshard online without downtime. It then maps all of it onto how Vitess, Citus, CockroachDB, DynamoDB and Cassandra actually partition data, and ends with a small simulation whose output you can reproduce.

What this covers: placement strategies, hash-ring math with measured imbalance numbers, shard key selection and hot partitions, scatter-gather and distributed transactions, the dual-write, backfill and cutover resharding pattern, per-engine partitioning behaviour, failure modes, and a practical checklist.

Context and Background

A single relational or key-value server scales vertically until it does not. The ceilings are mundane: the working set outgrows RAM, the write-ahead log saturates one disk, replication lag grows because one primary must serialise every write, or a maintenance operation such as a schema change or a failover takes too long on a multi-terabyte instance. Read replicas relieve read pressure but do nothing for write throughput or total dataset size. At that point the options are to move to a natively distributed store or to partition the data yourself. Partitioning has two flavours. Partitioning usually means splitting a table inside one server (PostgreSQL declarative partitions, for instance); sharding means the partitions live on different servers, each with its own CPU, memory, disk and failure domain.

Sharding is old, and so are the ideas behind it. Karger and colleagues described consistent hashing in 1997 for distributing web caching load across changing sets of servers. Thaler and Ravishankar published highest random weight (HRW) hashing, now usually called rendezvous hashing, in the same period. Lamping and Veach published jump consistent hash from Google in 2014. Amazon’s Dynamo paper (2007) and Cassandra’s design popularised ring-based placement with virtual nodes in databases. Meanwhile Google’s Spanner and its descendants (CockroachDB, YugabyteDB, TiDB) went a different way: ordered key ranges that split and merge automatically, each replicated with a consensus protocol.

So there are two broad ways to be “sharded” in 2026. In application- or middleware-managed sharding, you run many ordinary database servers and something above them (Vitess in front of MySQL, Citus as a PostgreSQL extension, a home-grown routing layer) maps keys to servers. In natively distributed databases, the storage engine itself splits data into ranges or partitions, balances them and replicates them, and you mostly influence placement through key design. The mechanics differ, but the questions are identical: what is the shard key, what is the placement function, how does it behave when membership changes, and how do queries that do not include the shard key get answered?

It helps to separate three concerns that get blurred in casual talk. Placement is the function from key to shard. Routing is how a client or proxy finds the shard once placement says where the key lives. Rebalancing is how data moves when placement changes. Most outages and most migration pain come from treating these as one thing.

Sharding is also not free of consequence for the rest of your architecture. Once data spans nodes, you inherit questions about isolation and replication topology that a single node hid from you. If you are weighing how shards behave across regions, our guide to multi-region active-active database architecture covers the replication and conflict side that sits on top of the partitioning decisions discussed here.

Reference Architecture: Placement, Routing and Rebalancing

Direct answer: Database sharding splits data by a shard key into independent partitions placed by a deterministic function (range, hash or directory lookup). A stable placement function, such as consistent or rendezvous hashing, minimises data movement when shards are added or removed, while a routing layer and an online rebalancing protocol keep applications unaware of the move.

Database sharding reference architecture with router, placement function and shard map

Figure 1: Reference architecture for database sharding. The router resolves a shard key through a placement function or directory, forwards to the owning shard, and falls back to scatter-gather when the key is absent.

The diagram shows the components that every sharded system has in some form, even when they are hidden inside the database. A client library or proxy receives a query, extracts the shard key and asks the placement function (or a lookup table) which shard owns it. If the query carries the key, it goes to exactly one shard. If it does not, the router must fan the query out to every shard and merge the results, which is the scatter-gather path and the dominant source of tail latency in sharded systems. A control plane owns the authoritative shard map, performs rebalancing and publishes map versions to routers.

Three families of placement

Range sharding assigns contiguous key intervals to shards: keys A to F on shard 1, G to M on shard 2, and so on. Its great virtue is that range scans and ordered queries touch few shards, because neighbouring keys live together. Its great vice is that monotonically increasing keys (timestamps, auto-increment IDs) send every new write to the last range, creating a hot tail. Range sharding is the natural fit when the access pattern is “give me everything between X and Y”, which is why Spanner-style systems, CockroachDB and TiKV use it, and why they invest heavily in automatic splitting to cope with hot ranges.

Hash sharding applies a hash function to the key and places the result by modulo, ring position or another scheme. A good hash spreads even sequential keys uniformly, which removes the hot-tail problem and is why it is the default for key-value access. The price is that range queries become scatter-gather, since adjacent keys are scattered randomly. Hash placement also makes the algorithm for handling membership change the central design question, which the next section addresses.

Directory (lookup) sharding keeps an explicit table that maps a key, or a bucket of keys, to a shard. It is the most flexible: you can move a single noisy tenant to a dedicated shard, place customers by region for data residency, or rebalance by editing a row. The cost is that the directory is itself a critical, highly available, heavily cached service, and its consistency determines whether two routers can disagree about where a key lives. Many production systems are hybrids: hash a key into one of a fixed number of logical buckets (say 4,096), then use a directory to map buckets to physical shards. That indirection is the single most useful trick in this article, and we return to it in the resharding section.

Why modulo hashing fails

The naive placement is shard = hash(key) mod N. It is uniform and trivially fast, and it is catastrophic under growth. When N changes to N+1, a key stays put only if hash mod N equals hash mod (N+1), which happens for about one key in N+1 on average for large hash spaces. Going from 10 to 11 shards therefore relocates about 10/11, or roughly 91 percent, of all keys. Our simulation later in the article measures 90.9 percent. A rebalance that touches 91 percent of the data is not a rebalance; it is a full reload. The remedy is a placement function with minimal disruption: when a node joins, only the keys that the new node now owns (about 1/(N+1) of them) should move, and they should move only to the new node.

Routing and map versioning

Routing deserves its own care. A router holds a cached copy of the shard map. During a resharding event the map changes, and a router with a stale copy may send a write to the old owner. Robust systems attach a map version or epoch to every request, and shards reject requests whose epoch is older than their own, forcing the router to refresh. Without this fencing, a slow router can silently write to a shard that has already handed the key range over, producing the kind of lost-update bug that only appears during migrations. Vitess handles this with routing rules and a topology service; Citus keeps its metadata in coordinator tables; CockroachDB uses a range descriptor cache that is corrected by the replicas themselves when a request lands in the wrong place. The mechanisms differ, but all of them solve the same stale-map problem.

Replication inside a shard

Each shard is itself a replicated unit. In middleware-managed designs, a shard is typically a primary with asynchronous or semi-synchronous replicas. In natively distributed designs, each range or partition is a consensus group (CockroachDB uses Raft per range) or a quorum-replicated token range (Cassandra, DynamoDB’s internal design). This matters for capacity planning because failure of a shard’s primary is a shard-local event: only that fraction of the keyspace is unavailable during failover. This containment is one of the strongest arguments for sharding beyond raw throughput, and it is the same blast-radius logic behind cell-based architecture and bulkheads, where shards double as failure-isolation cells.

Stable Placement: Consistent, Jump and Rendezvous Hashing

Three algorithms dominate hash placement with minimal disruption. They solve the same problem with different trade-offs in memory, lookup cost, balance and flexibility.

Consistent hashing with virtual nodes

In the Karger et al. scheme, both nodes and keys are hashed onto a circular space (commonly 0 to 2^64 – 1). A key belongs to the first node point found moving clockwise from the key’s position. When a node joins, it takes over only the arc between its predecessor and itself; when it leaves, its arc passes to its successor. Expected movement on a join is therefore about 1/(N+1) of the keys, which is the theoretical minimum.

The weakness is balance. With one point per node, arc lengths follow roughly an exponential-style distribution, so some nodes own several times the average and some own almost nothing. The fix is virtual nodes (vnodes): each physical node is hashed to many points, and its load is the sum of many small arcs, which concentrates around the mean. The standard deviation of load shrinks roughly with the square root of the number of points per node. Vnodes also spread a failed node’s load across many survivors instead of dumping it on one successor, which avoids cascading overload.

Consistent hashing ring with virtual nodes compared with rendezvous hashing scoring

Figure 2: Consistent hashing places keys on a ring of virtual-node points, while rendezvous hashing scores every node per key and picks the highest. Both move only about 1/(N+1) of keys when a node is added.

The ring costs memory and a binary search: with N nodes and V vnodes each, you keep N times V sorted points and each lookup is O(log(NV)). That is trivial at 100 nodes with 256 vnodes (25,600 points) and still cheap at thousands of nodes. The operational cost is subtler. More vnodes mean better balance but more token ranges to stream, repair and track; Cassandra’s experience illustrates this directly. Historically its default was 256 vnodes per node, and Cassandra 4.0 lowered the default num_tokens to 16 (CASSANDRA-13701) because very high vnode counts hurt repair and availability under multi-node failures, paired with a replica-aware token allocator (allocate_tokens_for_local_replication_factor) so that a small vnode count is still well balanced. Random token assignment with few vnodes is, in the words of one widely cited analysis of the change, hopeless for balance, which is why the smarter allocator is a necessary part of the trade.

Jump consistent hash

Jump hash (Lamping and Veach, 2014) computes the bucket in O(log N) time with zero memory: given a 64-bit key and a bucket count, it uses a linear congruential generator to “jump” forward through candidate buckets until it overshoots. Balance is essentially perfect and movement on growth is minimal. The catch is structural: buckets are numbered 0 to N-1, and you can only grow or shrink by adding or removing the highest-numbered bucket. You cannot remove an arbitrary node from the middle, so it suits systems where shards are numbered slots (a fixed-ordinal storage fleet) rather than arbitrary servers that fail and come back. It is a good fit for choosing among logical shards when physical placement is handled elsewhere.

Rendezvous (highest random weight) hashing

Rendezvous hashing is the most conceptually simple. For each key, compute a score hash(node_id, key) for every node and pick the node with the highest score. Remove a node and only the keys it was winning move, each to its runner-up. Add a node and it wins exactly the keys for which it now scores highest, about 1/(N+1) of them. Balance is excellent without vnodes, because each key is an independent draw among all nodes. Weighted nodes are straightforward by scaling the score, and the runner-up ordering gives you a free, deterministic replica list: the top-k scoring nodes are the k replicas, and they remain stable as the set changes.

The cost is O(N) hash computations per lookup. At N = 50 that is negligible; at N = 5,000 on a hot path it matters, and hierarchical variants (hash to a group first, then rendezvous within the group) reduce it. Rendezvous hashing is popular for cache selection, load balancing and for choosing among a modest number of storage nodes, and it avoids the ring-state synchronisation problem entirely: any party that knows the node list can compute placement independently.

A measured comparison

The Python simulation in the walk-through section below places 200,000 synthetic keys on 10 nodes and then adds an eleventh. Running it produces the following deterministic results (hash is BLAKE2b truncated to 64 bits; numbers are specific to this key set and are meant to illustrate the shape, not to be read as universal constants).

Scheme (10 nodes, 200k keys) Max load / mean Min load / mean Keys moved on 10 to 11
Ring, 1 point per node 3.08 0.00 not measured
Ring, 16 vnodes 1.67 0.70 not measured
Ring, 128 vnodes 1.12 0.76 8.9 %
Ring, 512 vnodes 1.02 0.97 not measured
Rendezvous (HRW) 1.01 0.99 9.1 %
Jump hash 1.01 0.99 9.1 %
Modulo hash mod N about 1.0 about 1.0 90.9 %

The ideal movement for 10 to 11 nodes is 1/11, or 9.1 percent. Three things stand out. A single point per node is unusable, with the heaviest node holding three times the mean and one node effectively empty. Sixteen vnodes still leave a 67 percent overload on the busiest node, which is exactly why Cassandra pairs low vnode counts with a smart allocator. And HRW and jump hash achieve near-perfect balance with no tuning parameter. The ring’s advantage is not balance; it is that lookup cost does not grow linearly with N and that its neighbour structure maps naturally onto replication (“the next R distinct nodes clockwise”).

Choosing among them

If you need arbitrary node add and remove, a small to moderate node count and no shared state, use rendezvous. If you need to scale to thousands of nodes with cheap lookups and already maintain a membership service, use a ring with enough vnodes (or a smart token allocator). If your shards are numbered slots that only grow, jump hash is the leanest option. If you want maximum operational control, none of these replaces the indirection pattern: hash into a fixed, large number of logical buckets (Redis Cluster uses 16,384 hash slots for this purpose) and manage a bucket-to-server table. The bucket count then caps your eventual scale but makes every rebalance an explicit, inspectable, reversible table edit rather than a side effect of an algorithm.

Shard Key Selection and Hot Partitions

The placement algorithm only decides where a given key value goes. The shard key decides what that value is, and no hash function can rescue a bad one. Shard key selection is the highest-leverage design decision in a sharded system because it is the hardest to change later: it appears in every table, every query and every join.

The four properties of a good shard key

A good key has high cardinality, so there are many more distinct values than shards; a country code with 200 values cannot evenly feed 500 shards. It has even frequency, meaning no single value dominates traffic or storage. It has query affinity, appearing as an equality predicate in the hot queries so that most requests hit one shard. And it is stable, meaning a row’s key never changes; a mutable shard key turns an UPDATE into a cross-shard delete and insert.

These properties conflict. User ID has high cardinality and good affinity for user-centred apps but may have a heavy tail: a few celebrity accounts or enterprise tenants generate most of the traffic. Tenant ID gives perfect isolation and joins within a tenant but concentrates a large tenant on one shard. Timestamp spreads reads poorly and concentrates writes on the newest range. There is no universal answer, only a deliberate choice backed by measured access patterns.

Composite keys and hash-prefixed range keys

A common compromise is the composite key: a hashed high-cardinality prefix for distribution plus a sortable suffix for range scans within the partition. DynamoDB’s partition key plus sort key is the canonical instance: the partition key is hashed to choose a partition, and the sort key orders items within it, so “all orders for customer 42 in March” is a single-partition range query. CockroachDB offers hash-sharded indexes for sequential keys, which prefix a computed bucket to spread a monotonic index across ranges at the price of scatter reads for time-ordered scans. Both illustrate the same trade-off: you buy write distribution by paying in range-scan locality.

Hot partitions and the celebrity problem

Even with a uniform hash, skewed access creates hot partitions. A viral item, a large tenant, or a time-based burst concentrates load on one key or key range. Mitigations fall into a few categories, in rising order of intrusiveness. Caching absorbs hot reads, since the hottest keys are the most cacheable. Adaptive systems can isolate hot items: DynamoDB’s adaptive capacity can raise throughput for a busy partition and can split partitions so that frequently accessed items do not share one, though a single item remains bound by the per-partition maximum (documented as 3,000 read units and 1,000 write units per second). Key salting appends a random or computed suffix (item42#0 to item42#9) so that writes to one logical key spread across ten physical keys, at the cost of reading all ten and merging. Dedicated placement moves a known-heavy tenant to its own shard through the directory.

Worked load-imbalance math

Suppose a hash-sharded table spreads K keys across S shards, and each key receives equal traffic. The count per shard is approximately binomial with mean K/S and standard deviation sqrt((K/S)(1 – 1/S)). Take illustrative numbers: K = 100,000 tenants on S = 64 shards gives a mean of 1,562 and a standard deviation near 39, so the heaviest shard is perhaps 3 standard deviations above the mean, around 1,680 or about 7.5 percent over. Uniform keys on many shards are well behaved.

Now make the traffic Zipf-like, which is closer to real life. If the top 1 percent of tenants (1,000 of them) generate 50 percent of all requests, then each top tenant carries on average 0.05 percent of total load, versus 0.0005 percent for a typical tail tenant, a 100-fold gap. With 64 shards the mean shard carries 1.56 percent of total load, and a shard unlucky enough to host just five of the top tenants adds 0.25 percent of total load, still modest. The danger is a single tenant dominating: if one tenant is 5 percent of all traffic, the shard that hosts it carries 5 + 1.5 = 6.5 percent against a 1.56 percent mean, an overload factor of over 4 that no placement function can fix. This is why the unit of balancing must be smaller than the unit of skew, and why a key with a heavy tail needs splitting, salting or isolation rather than more shards.

Hot partition and shard key selection decision flow with salting and isolation options

Figure 3: Decision flow for shard key selection. Start from the dominant query, test cardinality and skew, then choose a composite key, salting or tenant isolation.

The flow in Figure 3 captures the sequence experienced teams follow. Identify the dominant access pattern first, then test whether the candidate key has enough cardinality and an acceptable frequency distribution on real data (not on a schema diagram), then decide how to treat the tail. A shard key that looks fine at 10 shards often fails at 200 because the ratio of the biggest key to the per-shard mean grows with shard count.

Cross-Shard Queries and Transactions

Sharding converts a single-node problem into a distributed-systems problem, and the bill arrives in two places: queries and transactions.

Scatter-gather and tail latency

A query without the shard key must visit every shard. If each shard answers within its own latency distribution, the overall response is the maximum across shards, so tail latency grows with shard count. Illustrative arithmetic: if each shard has a 1 percent chance of a slow response (above your target), a query touching 100 shards has a 1 – 0.99^100, or about 63 percent, chance of being slow. Fan-out turns a rare per-shard event into the common case. Mitigations include hedged requests (send a duplicate to a replica after a short delay), per-shard timeouts with partial results where semantics allow, and reducing fan-out by designing secondary indexes that are themselves sharded by the lookup key.

Secondary indexes are the classic dilemma. A local index lives with each shard’s data, is cheap to maintain transactionally but requires scatter-gather to query. A global index is partitioned by the indexed attribute, gives single-shard lookups, but each write now touches two shards (the data shard and the index shard) and needs cross-shard coordination or eventual consistency. Vitess implements global-style lookups with lookup vindexes, which are separate tables mapping a non-shard-key column to the keyspace ID; they are updated alongside the base row, with a documented ordering and trade-off between consistency and cost. DynamoDB’s global secondary indexes are eventually consistent by design.

Aggregates and joins follow the same logic. Partial aggregation on each shard followed by a merge (sum of sums, count of counts, approximate distinct via sketches) keeps the data moved small. Joins across shards are best avoided by co-location: shard related tables by the same key so that matching rows share a shard. Citus makes this explicit with co-location groups, in which distributed tables with the same shard count and distribution column type place matching shards on the same worker, and small dimension tables become reference tables replicated to every node so joins to them are always local. Analytical engines face the same constraint at much larger scale; our look at ClickHouse 26.8 LTS versus 26.3 and its Iceberg support shows how columnar systems distribute queries and shuffle joins rather than avoiding them.

Distributed transactions

When a transaction spans shards, atomicity requires a commit protocol. The classic answer is two-phase commit (2PC): a coordinator asks all participants to prepare, and if all vote yes, tells them to commit. 2PC is correct but has a well-known blocking problem: if the coordinator fails between phases, prepared participants hold locks until it recovers. Each cross-shard commit also adds at least two network round trips plus durable log writes at every participant, so latency scales with the slowest participant and throughput suffers under contention.

Systems choose different points on the spectrum. Vitess supports atomic distributed transactions through a 2PC implementation with a transaction coordinator and a metadata manager, and its documentation describes the default as best-effort multi-shard commit unless atomic mode is enabled, with a latency cost. Spanner-style databases such as CockroachDB run a parallel-commit protocol layered on Raft, avoiding a separate prepare round trip in the common case. Citus supports distributed transactions through PostgreSQL’s two-phase commit and a distributed deadlock detector. DynamoDB offers TransactWriteItems and TransactGetItems for limited sets of items, with the documented trade-off of higher cost per operation. Cassandra offers lightweight transactions (compare-and-set via Paxos, with a newer Accord-based transaction protocol under development in the project; check the release notes for its current status) for single-partition conditional updates rather than general multi-partition ACID.

The practical rule is to design so that the 95 percent case stays on one shard, and to handle the remainder with either explicit 2PC or the saga pattern: a sequence of local transactions with compensating actions. Sagas give availability and avoid cross-shard locks but expose intermediate states and demand idempotent steps and careful compensation design. The right choice depends on whether intermediate states are acceptable to the business, not on which one sounds more modern.

Online Resharding: Dual-Write, Backfill, Verify, Cutover

Eventually the shard count is wrong. Data grew, a tenant outgrew its shard, or you moved from 8 to 64 shards. Resharding is where sharding either proves its worth or exposes every shortcut. The goal is a change in placement with no downtime, no lost writes and a path back if something looks wrong.

The canonical protocol

Almost every online resharding system, from Vitess’s VReplication to Citus’s logical-replication shard moves to home-grown pipelines, follows the same five stages.

1. Provision targets. Create the new shards and their schemas, empty, with the new placement function or shard map defined but not yet routed to.

2. Backfill a consistent snapshot. Copy existing rows from source shards to target shards according to the new placement. The snapshot must be consistent with a known position in the source’s change log (a binlog coordinate in MySQL, a log sequence number in PostgreSQL), so that stage 3 can resume exactly where the snapshot ended. Copying is throttled and chunked by primary key to bound its impact on live traffic.

3. Stream changes (dual-write or CDC). From the recorded position, apply every subsequent change from source to target. This can be implemented as change data capture reading the log, which is the preferred approach because it adds no application complexity and cannot miss a write, or as application-level dual-writes, where the service writes to both old and new placements. Dual-writes are riskier: a crash between the two writes leaves them divergent, and ordering across concurrent writers can differ between the copies. If you must dual-write, make writes idempotent and reconcile with a verification pass.

4. Verify. Compare source and target. Row counts and checksums over key ranges catch most problems; a row-by-row diff catches the rest. Vitess ships VDiff for this, comparing source and target tables and reporting differences before you switch. Verification should run while replication is live and again just before cutover.

5. Cut over, with a way back. Briefly stop writes to the affected keys, wait for the target to catch up to the source position, flip the routing so reads and writes go to the new placement, and resume. Keep replication running in the reverse direction (target to source) so that if the new shards misbehave, you can flip back without data loss. Only after a soak period do you retire the old copy.

Online resharding sequence with backfill, change streaming, verification and cutover with reverse replication

Figure 4: Online resharding sequence. A consistent snapshot is backfilled, changes stream continuously, a diff verifies parity, and a short write pause precedes a routing flip protected by reverse replication.

Vitess is a good concrete example of the pattern, and its documentation is explicit. A MoveTables workflow is created, monitored with status (which reports percentage copied and replication lag), verified with VDiff, then cut over with SwitchTraffic, which briefly pauses writes on the source primary while the target catches up, so a timeout setting matters. By default SwitchTraffic also starts a reverse replication stream with the target as the source, and ReverseTraffic can move traffic back; disabling reverse replication means you cannot roll back after writes have moved. The final complete step is described as destructive because it removes the workflow artefacts and, by default, drops the source tables. Citus follows the same logic with PostgreSQL logical replication: since version 11.0 the shard rebalancer in the Community edition performs non-blocking moves, holding only a brief write lock on the shard while metadata is updated, and it requires tables to have a primary key or a configured replica identity so that logical replication can apply updates and deletes.

Resharding strategies by placement

How painful resharding is depends on your placement. With modulo placement, changing N moves nearly everything, so teams avoid it by choosing a huge N up front. With fixed logical buckets (virtual shards), you change only the bucket-to-server table: moving bucket 713 from server A to server B is a small, local backfill, and you can do it one bucket at a time under load. This is why many mature deployments pre-split into far more logical shards than they need physical servers; Citus guidance for multi-tenant applications is to choose 32 to 128 shards so that you can later spread them across more workers without splitting data, since moving a whole shard is much simpler than splitting one. With range placement, the unit of change is a split or merge of ranges, which natively distributed systems do automatically.

Splitting a shard is harder than moving one, because every row must be re-evaluated against the new placement and may land in either child. That is why headroom matters: a shard split in a hot, nearly full shard competes with production load at exactly the wrong moment. The cheapest resharding is the one you scheduled when the system was at 40 percent capacity, not 90.

Operational controls

Throttle backfill by replication lag and by shard CPU, not by wall clock. Make the copy resumable from a checkpoint, because a multi-terabyte copy will be interrupted. Pause application schema changes during resharding unless your tool supports them. Rehearse cutover in a staging environment with realistic write rates, and measure the write pause: it should be seconds, dominated by catch-up lag, so keep lag low before you flip. Finally, define explicit abort criteria in advance, such as error rate or p99 latency thresholds, so the decision to roll back is mechanical rather than argued under stress.

How Real Engines Partition Data

The table below summarises placement strategy per engine. Details were checked against vendor documentation for this article; behaviours change between versions, so confirm against the documentation for the release you run.

Engine Placement model Unit of movement Resharding mechanism
Vitess (MySQL) Vindexes map a column to a keyspace ID; shards own keyspace ID ranges (hash vindex typical) Shard (key range) or table VReplication: MoveTables, Reshard, with VDiff, SwitchTraffic and reverse replication
Citus (PostgreSQL) Hash distribution on a column into a fixed number of shards (hash-token ranges) Shard placement Logical replication based, non-blocking since 11.0; rebalancer moves shards, co-located shards move together
CockroachDB Ordered key ranges (spans), replicated by Raft per range Range of roughly 512 MiB or less Automatic split when a range grows past its threshold and automatic rebalancing of replicas; hash-sharded indexes optional
DynamoDB Partition key hashed to a partition; sort key orders within; service-managed Partition (service managed) Transparent splits for size and throughput; adaptive capacity for hot partitions
Cassandra Murmur3 partitioner on a token ring with vnodes; replicas are next nodes on the ring Token range Add node, streaming of ranges; num_tokens must not change after a node joins

Vitess hashes the shard key through a vindex (for example a xxhash or hash vindex) into a keyspace ID, and each shard owns a contiguous range of keyspace IDs, written as hex ranges like -80 and 80-. Splitting a shard means dividing its range and filling the two children by VReplication. Because the range, not the modulo, defines ownership, a split touches only one shard’s data.

Citus distributes a table by hashing the distribution column into a fixed number of shards (the shard count is chosen at table creation time) and placing those shards across worker nodes. Because the number of shards is fixed and larger than the number of workers, adding a worker means moving whole shards, which is exactly the logical-bucket indirection discussed earlier. The cost is that the shard count caps parallelism, and changing it on an existing table means re-distributing the data (Citus provides functions for this, but it is a rewrite-class operation, so choose the count with growth in mind).

CockroachDB has no user-visible shard key in the usual sense; the primary key defines an ordered keyspace that the system divides into ranges, replicating each through Raft. When a range exceeds its size threshold (512 MiB in the documentation consulted, configurable through zone settings) it splits, and the allocator rebalances replicas for size, load and locality. Hot ranges are also split by load. The consequence for your design is inverted compared with hash systems: you rarely choose a placement algorithm, but you must avoid monotonically increasing primary keys that concentrate inserts on one range.

DynamoDB hashes the partition key internally, and the service splits partitions as data or throughput grows, entirely hidden from you. Its adaptive capacity lets a hot partition borrow unused throughput from the table’s provisioned total, and it can isolate frequently accessed items. Design guidance remains the same as everywhere: choose a high-cardinality partition key, and expect throttling if one key value exceeds the per-partition ceiling.

Cassandra places a row by taking the Murmur3 hash of the partition key to a token and assigning it to the node owning that token range, with replicas on the next distinct nodes. Capacity grows by adding nodes that stream ranges from existing owners, and the vnode count trades balance against repair cost and availability, as discussed above.

For vector and search workloads the same questions reappear in new forms. If you shard an approximate-nearest-neighbour index, a query must usually probe every shard, which makes vector search an inherently scatter-gather workload; our comparison of pgvectorscale and pgvector with StreamingDiskANN covers the single-node side before you reach for sharding.

Walk-through: Simulating Placement and Movement

The script below reproduces the numbers in the comparison table. It uses only the Python standard library, so you can change the node count, key count or vnode counts and see how placement behaves before committing to a design.

import hashlib, bisect

def h(s):
    return int.from_bytes(
        hashlib.blake2b(s.encode(), digest_size=8).digest(), "big")

class Ring:
    """Consistent hashing ring with virtual nodes."""
    def __init__(self, nodes, vnodes):
        self.pts = sorted((h(f"{n}#{v}"), n)
                          for n in nodes for v in range(vnodes))
        self.keys = [p for p, _ in self.pts]
    def get(self, key):
        i = bisect.bisect(self.keys, h(key)) % len(self.keys)
        return self.pts[i][1]

def hrw(nodes, key):
    """Rendezvous hashing: highest score wins."""
    return max(nodes, key=lambda n: h(n + "|" + key))

def jump(key, n):
    """Jump consistent hash (Lamping and Veach, 2014)."""
    b, j = -1, 0
    key &= 0xFFFFFFFFFFFFFFFF
    while j < n:
        b = j
        key = (key * 2862933555777941757 + 1) & 0xFFFFFFFFFFFFFFFF
        j = int((b + 1) * (1 << 31) / ((key >> 33) + 1))
    return b

keys = [f"user:{i}" for i in range(200_000)]
nodes = [f"n{i}" for i in range(10)]
grown = nodes + ["n10"]

moved = lambda f, g: sum(f(k) != g(k) for k in keys) / len(keys)
r1, r2 = Ring(nodes, 128), Ring(grown, 128)
print("ring   ", moved(r1.get, r2.get))
print("hrw    ", moved(lambda k: hrw(nodes, k), lambda k: hrw(grown, k)))
print("jump   ", moved(lambda k: jump(h(k), 10), lambda k: jump(h(k), 11)))
print("modulo ", moved(lambda k: h(k) % 10, lambda k: h(k) % 11))

In the run used for this article the output was approximately 0.089 for the ring, 0.091 for rendezvous, 0.091 for jump hash and 0.909 for modulo, against an ideal of 0.0909. The ring is slightly below the ideal here only because vnode arcs are random; with a different key set it can land slightly above. To see imbalance, count keys per node with collections.Counter and divide the maximum by the mean: change the vnode count from 1 to 16 to 128 to 512 and watch the maximum fall from roughly 3.1 times the mean to about 1.02.

Two extensions are worth trying. First, replace the uniform key set with a Zipf-distributed access pattern (sample keys with weights proportional to 1/rank) and measure request imbalance rather than key imbalance; you will see that a perfectly balanced key count can still produce a hot shard, which is the lesson of the shard key section. Second, add weights to rendezvous hashing by scoring -weight / ln(u) where u is the hash normalised to (0, 1); a node with weight 2 then wins roughly twice as many keys, which models heterogeneous hardware without any vnode bookkeeping.

A routing layer in miniature

A directory-based router with logical buckets is only a few lines more and demonstrates the indirection pattern from the reference architecture.

BUCKETS = 4096
bucket_map = {b: f"shard{b % 8}" for b in range(BUCKETS)}   # directory
epoch = 1

def route(key):
    b = h(key) % BUCKETS          # stable forever
    return bucket_map[b], epoch   # shard + map version for fencing

def move_bucket(b, target):
    global epoch
    # 1) backfill + stream bucket b to target (omitted)
    # 2) verify, pause writes for b, catch up
    bucket_map[b] = target
    epoch += 1                    # shards reject older epochs

The point is that the hash never changes; only the table does. Adding an eighth to ninth shard is a loop over 4096/9 buckets, each moved independently, in any order, pausable and reversible. That is the property the resharding section kept returning to, and it is what Redis Cluster’s 16,384 hash slots, Citus’s fixed shard count and Vitess’s keyspace-ID ranges each provide in their own way.

Trade-offs, Gotchas, and What Goes Wrong

Premature sharding. Sharding multiplies operational surface: schema migrations on N servers, N backups, N upgrade paths, cross-shard reporting. A single well-tuned primary with read replicas and good indexing carries a surprising amount of load, and a natively distributed database may remove the need to build the routing yourself. Shard when measurements show a ceiling you cannot buy your way past, not because the architecture diagram looks more serious.

Changing the shard key later. The shard key is embedded in every table and query. Changing it is effectively a new system with a data migration, and it is the most expensive mistake in this domain. Prototype the dominant queries against the candidate key before you commit.

Hot shards masquerading as capacity problems. Adding shards does not fix a single hot key. Teams burn weeks scaling out a cluster whose 95th-percentile shard is saturated by one tenant. Look at per-shard and per-key load before adding hardware.

Unbounded fan-out. A feature added in a hurry that queries without the shard key works on ten shards and falls over on two hundred. Make scatter-gather queries visible: tag them in logs and budget them in code review.

Stale routing. Without map versioning and fencing, a router with an old shard map writes to the previous owner during cutover, and the write vanishes when the old copy is dropped. Epoch checks and a write pause during the final catch-up exist precisely for this.

Dual-write divergence. Application-level dual-writes fail partially. If you use them, treat the source of truth as one side only, make writes idempotent and run continuous verification. Prefer log-based change capture.

Backfill that harms production. An unthrottled copy competes for I/O and buffer cache with live traffic, inflating latency for everyone. Throttle on replication lag and p99, and schedule heavy phases during low traffic.

Auto-increment and global uniqueness. Per-shard sequences collide. Use globally unique IDs: UUIDs, time-ordered IDs such as ULID or snowflake-style generators with embedded shard bits, or a sequence service. Time-ordered IDs reintroduce the hot-tail problem under range placement, so pair them with hash placement or a hash-prefixed key.

Over-splitting. Too many tiny shards or ranges increases metadata, connection counts and per-range overhead. In consensus-per-range systems, every range carries heartbeat and replication overhead, so very high range counts per node become a scaling dimension of their own.

Practical Recommendations

Start by asking whether you need to shard now. If your dataset fits in a single instance with headroom and the ceiling is read throughput, add replicas and caching first. If the ceiling is write throughput or total size, decide between a natively distributed database and middleware-managed sharding by weighing how much of the placement, rebalancing and cross-shard transaction machinery you want to own. Owning it buys control, such as moving a single tenant to a dedicated shard or pinning data to a region, at the cost of operations.

Design the shard key from the query log, not from the schema. Pick the attribute that appears as an equality predicate in the queries that matter, confirm its cardinality and skew on production data, and prefer a composite key that gives distribution plus local ordering. Plan the tail before launch: identify the top tenants or keys and decide whether they will be salted, isolated or cached.

Always put a layer of indirection between the hash and the machine. Use a large, fixed count of logical buckets or virtual shards and a mapping table, so that capacity changes are bucket moves rather than rehashes. If you must pick an algorithm for the machine layer, use rendezvous hashing for modest node counts, a ring with a smart allocator for large ones, and jump hash for append-only numbered slots.

Build resharding as a product feature before you need it, and rehearse it.

  • [ ] Shard key chosen from measured query patterns, with cardinality and skew tested on real data
  • [ ] Hash into fixed logical buckets, with a bucket-to-shard directory and versioned epochs
  • [ ] Globally unique ID scheme that does not rely on per-shard sequences
  • [ ] Cross-shard queries tagged, budgeted and alerting on fan-out
  • [ ] Resharding runbook: snapshot backfill, change streaming, verification, short write pause, reverse replication, soak period
  • [ ] Abort criteria defined in numbers (error rate, p99, lag) before the cutover
  • [ ] Per-shard and per-key load dashboards, not just cluster averages
  • [ ] Capacity headroom policy so splits happen at 40 to 60 percent utilisation

Frequently Asked Questions

What is database sharding and when should I use it?

Database sharding splits a dataset across multiple independent database servers using a shard key, so each server stores and serves only part of the data. Use it when a single primary cannot handle write throughput, dataset size or recovery time even after vertical scaling, indexing, read replicas and caching. It also helps isolate failures and place data by region. Do not adopt it earlier than necessary, because it adds routing, rebalancing and cross-shard complexity.

What is the difference between consistent hashing and rendezvous hashing?

Consistent hashing places nodes (usually many virtual points each) and keys on a ring and assigns a key to the next node clockwise. Rendezvous hashing scores every node for every key and picks the highest. Both move about 1/(N+1) of keys when a node is added. Rendezvous needs no ring state and balances well without vnodes, but costs O(N) per lookup; a ring has cheaper lookups at large N but needs vnodes for balance.

Why is modulo hashing a bad choice for sharding?

With hash(key) mod N, changing N changes the result for nearly every key, because a key stays put only when both moduli agree. Going from 10 to 11 shards relocates about 91 percent of keys, as the simulation above shows, versus the ideal 9 percent. That forces a near-complete data reload on every scale event. It is acceptable only if the shard count is fixed forever or if you hash into a large fixed bucket space first.

How do you reshard a database without downtime?

Provision target shards, backfill a consistent snapshot, then stream every subsequent change using change data capture or idempotent dual-writes. Verify parity with checksums or a row diff, pause writes briefly while the target catches up, then flip routing to the new placement. Keep reverse replication running so you can roll back, and retire the old copy only after a soak period. Vitess MoveTables and Citus shard moves follow this pattern.

How do I choose a good shard key?

Choose a key with high cardinality, an even frequency distribution, presence as an equality predicate in your hottest queries, and immutability. Validate it on production data, looking at the heaviest values, because a heavy-tailed key causes hot shards that more hardware cannot fix. Composite keys, such as a hashed tenant prefix plus a sortable suffix, often balance distribution with range-scan locality. Avoid raw timestamps or sequences as the leading component under range placement.

Do sharded databases support transactions across shards?

Many do, at a cost. Cross-shard atomicity needs a commit protocol such as two-phase commit, adding network round trips and lock-holding time, so throughput and tail latency worsen. Vitess, Citus, CockroachDB and DynamoDB each offer some form of distributed transaction with different guarantees and limits. The usual guidance is to keep the majority of transactions on a single shard through co-location and shard key design, and to use sagas or 2PC for the remainder.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *