The idea: one collection, many shards

When a collection is too big or too busy for one replica set, MongoDB can shard it: cut it into pieces and spread the pieces over several replica sets, the shards. The app does not see the cut. It connects to a router called mongos, which sends each command to the shard or shards that hold the data. mongos keeps no data. It knows where things are from the config servers, a small replica set that stores the cluster's metadata.

The app sends a query to mongos. mongos has a cached copy of config.chunks from the config servers and refreshes it when it is stale. Below, three shards s0, s1 and s2 each hold two chunks: s0 holds Min to 100 and 100 to 200, s1 holds 200 to 300 and 300 to 400, s2 holds 400 to 500 and 500 to Max.
mongos routes with a cached copy of the chunk map; the config servers hold the real one.

The canvas shows the collection shop.orders sharded on {customerId: 1} over three shards, two chunks each. The top row has the app, mongos with its cached routing table, and the config servers with config.chunks and the balancer. The bottom row has the shards and the chunks they own, with their sizes. Every message between them flies as a box. How a single mongod runs a command inside (WiredTiger, journal, oplog) is on How MongoDB Runs a Command; this page stays one level up.

Shard key and chunks

You shard a collection with sh.shardCollection("shop.orders", {customerId: 1}). The shard key is one or more fields that every document has. Its value space, from MinKey to MaxKey (written Min and Max on the canvas), is cut into chunks: contiguous ranges such as [100, 200), lower bound included, upper bound excluded. Each chunk belongs to exactly one shard. MongoDB 7.0 and later call them ranges in the documentation; the metadata collection is still config.chunks.

Each row of config.chunks has the range, the owning shard and a version called lastmod, written major|minor. The highest version in the collection is the collection version; the highest among one shard's chunks is that shard's version. The collection also has an epoch, an id that changes only if the collection is dropped or resharded.

mongos and its routing table cache

Reading config.chunks for every query would make the config servers a bottleneck, so each mongos keeps a copy in memory. On the canvas it starts at v1|5, the same as the config servers. Nothing pushes changes to mongos: the copy can fall behind, and the column on the right of the cache marks rows that no longer match (old (split) or wrong shard). That is fine, because every request mongos sends carries the version it believes the shard is at, and the shard checks it (see Versions and StaleConfig).

Targeted queries and scatter-gather

If the filter contains the shard key, mongos can tell which chunk holds the answer. find({customerId: 470}) falls in [400, 500), so it goes to s2 and nowhere else: a targeted query (Demo: Query with the shard key). s0 and s1 do no work at all.

If the filter has no shard key, any shard may hold matching documents. mongos sends the query to every shard and combines the answers: a scatter-gather (or broadcast) query. For find({status: "open"}).sort({orderDate: -1}).limit(3) each shard sorts its own matches and returns its first 3, and mongos does a merge sort of the three lists and keeps 3 (Demo: Query without the shard key: Oct 18 and Oct 16 from s1, Oct 15 from s2). s0 worked as hard as the others, and none of its rows made the result. A cluster with 30 shards does 30 times the work for such a query, and it is only as fast as the slowest shard.

Left: find with customerId 470 goes from mongos only to s2, because 470 is in the chunk 400 to 500; s0 and s1 are greyed out. Right: a find on status with a sort and a limit goes from mongos to s0, s1 and s2, and mongos merges three sorted lists.
With the shard key one shard works; without it every shard works and mongos merges.
QueryShards askedWhy
{customerId: 470}1equality on the shard key
{customerId: {$gte: 100, $lt: 300}}2 (range key), all (hashed key)a range covers neighbouring chunks only if the chunks follow the key order
{status: "open"}allno shard key in the filter
insertOne({customerId: 470, …})1every document has a shard key value

Chunk splits and jumbo chunks

Chunks have a target size, the chunk size: 128 MB by default since MongoDB 6.0 (64 MB before). On the canvas, when an insert pushes a chunk past 128 MB, the shard looks for the shard key value that cuts it into two halves of similar size (the splitVector command) and asks the config servers to split there. In Demo: Insert past 128 MB, 70 MB of orders for customer 580 make [500, Max) 130 MB. It is split at 580 into [500, 580) 60 MB at version 1|6 and [580, Max) 70 MB at 1|7.

A split changes only metadata: no document moves, both halves stay on s2, and only the minor number of the version grows. A chunk whose documents all have the same shard key value can't be split at all, because there is no key between them. MongoDB marks it jumbo, and the balancer leaves it where it is. Press insert 20 MB for one customer a few times to see one appear. A low-cardinality shard key (a country, a status) makes jumbo chunks likely.

The balancer and moveChunk

Splits keep chunks small but do nothing about where they live. The balancer, a background process on the config server primary, compares the shards and moves chunks from the fullest to the emptiest. The canvas uses the classic rule: move one chunk when the chunk counts of the busiest and the emptiest shard differ by at least 2 (the threshold for collections with fewer than 20 chunks; 4 up to 79 chunks, 8 above). In Demo: The balancer migrates a chunk two splits leave s2 with 4 chunks against 2 and 2, so [400, 450) (35 MB) moves from s2 to s0.

A move, moveChunk, is run by the donor shard and takes these steps:

StepWhat happensReads and writes for the chunk
1. startthe balancer asks the donor (s2) to move the chunk to the recipient (s0)go to the donor
2. clonethe recipient copies the chunk's documents from the donor in batchesgo to the donor
3. catch upthe donor logged the writes made during the clone; the recipient applies themgo to the donor
4. critical sectionthe donor blocks writes to the chunk so nothing changeswrites wait (briefly)
5. committhe donor asks the config servers to commit: the chunk now belongs to the recipient, version 2|0go to the recipient
6. clean upthe donor's copy is an orphan; the range deleter removes it after orphanCleanupDelaySecs (900 s)orphans are filtered out

The commit bumps the major version: the moved chunk becomes 2|0, and one chunk that stays on the donor gets 2|1 (on the canvas [450, 500)), so that both shards' versions change.

Versions and StaleConfig

After the move, mongos still caches v1|5 and believes [400, 500) is on s2. Nobody told it, and nobody has to. When it sends find({customerId: 420}) to s2, it attaches s2's version as it knows it, 1|5. s2 is at 2|1. The major numbers differ, so s2 refuses with a StaleConfig error. mongos then refreshes: it asks the config servers only for the chunks with lastmod at least 1|5 (4 rows here), updates its cache to v2|1, and retries, this time at s0 (Demo: Stale mongos). The app sees a normal answer, a little later.

A split does not trigger this. After Demo: Insert past 128 MB the cache is still v1|5 while s2 is at 1|7, and s2 accepts it: only the minor number differs, and both halves of the old chunk are still on s2, so mongos's route is still right. Only the major number (and the epoch) has to match. One catch: the halves of a split take the collection's major number. Once a migration has raised it to 2, a split on a shard still at 1|x lifts that shard to 2|… too, and mongos's next request there gets a StaleConfig.

Choosing a shard key: increasing vs hashed

A shard key whose new values always grow, such as an ObjectId _id, a timestamp or a counter, sends every insert to the chunk that ends at MaxKey. In Demo: Increasing key five new customers 601 to 605 all land on s2, which takes 5 of 5 writes while s0 and s1 take none. That shard is a hot shard: adding shards does not add write throughput, and the balancer keeps having to move chunks away from it.

A hashed shard key, sh.shardCollection("shop.orders", {customerId: "hashed"}), routes on a hash of the value instead (a 64-bit number from MD5; the canvas uses 0–999). Neighbouring ids get unrelated hashes, so the same five inserts spread 1, 2 and 2 over s0, s1 and s2 (Demo: Hashed key). Equality queries are still targeted, because mongos can hash 470 too. Range queries on the key are not: customerId 100 to 200 is spread over every chunk, so it becomes scatter-gather.

Left: with a range key, the inserts for new customers 601 to 605 all go to s2, drawn red with 5 inserts, while s0 and s1 get 0. Right: with a hashed key, s0 gets 1 insert, s1 gets 2 and s2 gets 2.
An increasing range key makes one hot shard; hashing the same key spreads the writes.
Range keyHashed key
Equality on the keytargetedtargeted
Range on the key, sort by the keytargeted to neighbouring chunksscatter-gather
Increasing values (ObjectId, time)✗ one hot shard✓ spread
Data locality (one customer's orders together)✓✓ per value, but neighbours are scattered

A good key has many distinct values, spreads writes, and appears in most queries. A compound key such as {customerId: 1, orderDate: 1} keeps one customer's orders together and still lets a big customer's chunk split. Since MongoDB 5.0 reshardCollection can change the key of a live collection, at the cost of copying all of it. For the general idea of range vs hash placement, see Range vs Hash Partitioning and Consistent Hashing; for partitioning on one server vs sharding over many, see Partitioning vs Sharding.

What changed in MongoDB 6.0 and later

The canvas follows the classic behaviour, which is easier to watch. Newer versions work differently in two ways:

  • No automatic splits (6.0.3 and later). Inserts no longer split chunks. A range can grow past the chunk size; the balancer splits off a piece only when it needs to move one.
  • Balancing on data size (6.0.3 and later). The balancer compares the amount of data per shard, not the number of chunks. A collection counts as balanced while the difference between the biggest and the smallest shard is less than 3 × the chunk size, 384 MB with the default 128 MB.

Routing, versions, moveChunk and StaleConfig work as shown.

What the page leaves out

  • Each shard and the config servers are replica sets of 3 members; the canvas draws one box each. Elections and write concern are on the MongoDB page.
  • Several mongos instances (each with its own cache), and the shard's own copy of the routing table (its filtering metadata) that it uses to drop orphans from results.
  • Zones (pinning key ranges to shards), the config shard of MongoDB 8.0, mergeChunks and automatic merging, balancer windows, and forceJumbo.
  • Distributed transactions across shards, $lookup and aggregation stages that run on mongos or a merging shard, and change streams on a sharded cluster.
  • The sizes are rounded and every insert is one big batch, so that a split happens in one step.