Choosing a MongoDB shard key, and defending it
"Pick a shard key for this workload and defend it."
What it is
A shard key is the field or set of fields MongoDB uses to partition documents across
shards. The cluster splits the key space into chunks, and the balancer distributes
chunks across shards. Every routed operation either names the shard key, in which case
mongos sends it to one shard, or does not, in which case it is broadcast to all of
them.
Three properties decide whether a key works, and they trade against each other:
| Property | Means | Failed by |
|---|---|---|
| Cardinality | How many distinct values exist | country in a Canada-only product: 1 chunk, forever |
| Frequency | How evenly values are distributed | customer_id where one customer is 40% of traffic |
| Monotonicity | Whether values increase over time | _id (ObjectId), timestamps, auto-increment |
Commonly confused with an index. The shard key must be backed by an index, but its job is different: an index decides how fast a query is on one shard, a shard key decides which shards the query touches at all. A perfect index on a badly-sharded collection still broadcasts.
Also commonly confused with the ESR rule (Equality, Sort, Range), which is guidance for compound index field ordering. It is related but not the same question, and using the term for shard keys is a tell.
The problem it solves
A shard key that fails any of the three properties produces one of three specific outages.
Low cardinality: you cannot split. MongoDB cannot split a chunk below a single
shard-key value. If status has four values, you have at most four chunks and cannot
use more than four shards, however much data arrives. The collection stops scaling and
no amount of hardware fixes it.
Low frequency spread: one shard takes the load. With customer_id where a single
enterprise customer is 40 percent of writes, that customer's chunk cannot be split
(same value) and it lands on one shard. That shard saturates while the others idle.
This is the jumbo chunk problem: a chunk too large to move and too uniform to
split.
Monotonic: every insert goes to one shard. With an increasing key, every new
document has the highest value, so every insert lands in the chunk covering
(lastValue, MaxKey), which lives on exactly one shard. You have an N-shard cluster
with single-shard write throughput. This is by far the most common mistake, because
_id looks like a natural shard key and ObjectId is monotonic in its leading
timestamp bytes.
Mechanics
Ranged, hashed, and the trade
// Ranged sharding: chunks cover contiguous key ranges.
sh.shardCollection("shop.orders", { customer_id: 1, order_date: 1 })
// Hashed sharding: MongoDB hashes the key, so chunks cover hash ranges.
sh.shardCollection("shop.events", { _id: "hashed" })
Hashing solves monotonicity completely: consecutive values hash to unrelated buckets,
so inserts spread evenly. And it destroys range queries, because
order_date >= X no longer corresponds to any contiguous chunk range, so it
broadcasts to every shard.
RANGED on {order_date: 1}
Query: order_date between Jan 1 and Jan 7
-> touches 1-2 shards. Excellent.
Insert: every new order has today's date
-> all inserts to one shard. Terrible.
HASHED on {order_date: "hashed"}
Query: order_date between Jan 1 and Jan 7
-> broadcast to all shards, merge, sort. Terrible.
Insert: hashes spread evenly
-> uniform write distribution. Excellent.
That symmetry is the core tension, and the resolution is almost always a compound shard key.
The compound key pattern that usually wins
// High-cardinality, evenly-distributed prefix; range-friendly suffix.
sh.shardCollection("shop.orders", { customer_id: 1, order_date: 1 })
customer_idfirst gives cardinality and spreads writes, because different customers hash into different chunks naturally.order_datesecond lets the range within a customer stay contiguous, so "this customer's orders in January" is a targeted query.- The most common query, "orders for customer X", names the prefix and routes to one shard.
The rule: the prefix must be present in your dominant query, or nothing routes. A
compound shard key only targets a query when the query includes the prefix fields. A
query on order_date alone still broadcasts.
Hashed prefix with a ranged suffix
When the natural prefix is monotonic and you still want locality:
// Since MongoDB 4.4, only the prefix field may be hashed.
sh.shardCollection("iot.readings", { device_id: "hashed", ts: 1 })
Writes spread by device, and readings for one device over a time range stay together. This is the standard time-series-on-Mongo answer.
Diagnosing a bad key in a running cluster
// Is the data distributed? Chunk counts per shard.
db.orders.getShardDistribution()
// Shard shardA: 1,204 chunks, 480 GB, 68% of docs <- imbalanced
// Shard shardB: 312 chunks, 110 GB, 16%
// Shard shardC: 298 chunks, 108 GB, 16%
// Are queries targeted or broadcast? Look at SHARD_MERGE vs SINGLE_SHARD.
db.orders.find({ order_date: { $gte: ISODate("2024-01-01") } }).explain()
// "shards": [ shardA, shardB, shardC ] <- broadcast
// Jumbo chunks: too big to move, too uniform to split.
use config
db.chunks.find({ jumbo: true })
getShardDistribution plus a broadcast-vs-targeted check on the top three queries is
the whole diagnosis, and doing it in that order is the answer to "how would you tell".
Resharding: the escape hatch, and its cost
Before MongoDB 5.0, a wrong shard key meant dumping and reloading the collection. Since 5.0:
db.adminCommand({
reshardCollection: "shop.orders",
key: { customer_id: 1, order_date: 1 }
})
The mechanics matter for the defence: MongoDB clones the collection into the new distribution while applying ongoing writes, then cuts over. So it needs approximately the size of the collection in free space on the cluster, it runs for hours to days on a large collection, and there is a brief write-blocking window at the cutover. MongoDB 8.0 added the ability to reshard without duplicating the entire collection when only the key ordering changes, which reduces but does not remove the cost.
The honest framing: resharding turned a career-limiting mistake into an expensive maintenance operation. It is not a reason to choose carelessly.
Zone sharding, for residency
sh.addShardToZone("shardEU", "EU")
sh.updateZoneKeyRange("shop.users", { region: "EU", user_id: MinKey },
{ region: "EU", user_id: MaxKey }, "EU")
With {region: 1, user_id: 1}, EU users' data physically lives on EU shards. This is
the mechanism for data residency requirements, and it constrains the shard key: the
residency field must be the prefix.
A worked example
An order service. 400 million orders, growing 2 million a day. Six shards. Query profile from the actual profiler, not from assumptions:
Query share current behaviour
-----------------------------------------------------------------------
find orders by customer_id, recent first 62% broadcast
find one order by order_id 21% broadcast
orders in a date range for reporting 9% broadcast
find by status = 'pending' for the fulfiller 6% broadcast
aggregate revenue by region and month 2% broadcast
Writes: 2M inserts/day, plus ~600k status updates/day
Current shard key: {_id: 1} (ObjectId, ranged)
Diagnosis. ObjectId is monotonic, so every one of the 2 million daily inserts lands
on the shard holding the top chunk. getShardDistribution confirms it: that shard is
at 71 percent CPU while the others sit at 12. And because no query names _id except
the 21 percent case, everything else broadcasts.
Evaluating candidates:
{customer_id: 1}
Cardinality 4.2M customers. Good.
Frequency top customer = 3.1% of orders; top 20 = 22%.
Not fatal, but one customer will produce a large chunk.
Monotonic No. Good.
Routes 62% of queries. Excellent.
Problem A single customer's orders can exceed the chunk size and
become jumbo, because all their orders share one key value.
{customer_id: 1, order_date: 1}
Cardinality Effectively unbounded. The date suffix means a heavy
customer's orders split across chunks by date.
Frequency Solved by the suffix.
Monotonic Not globally. New orders for different customers spread.
Routes 62% (customer queries, prefix present) targeted, and
"customer X in January" is a range within one shard.
Problem The 9% reporting queries on date alone still broadcast.
{_id: "hashed"}
Cardinality Perfect. Frequency perfect. Monotonicity solved.
Routes Only the 21% order_id lookups. Everything else broadcasts,
including the dominant 62%.
Verdict Fixes writes, makes reads worse. Wrong trade here.
Choice: {customer_id: 1, order_date: 1}. And the defence, which is the actual
answer to the drill:
Cardinality is 4.2 million on the prefix and effectively unbounded with the suffix, so we can split as far as we ever need to. Frequency: the top customer is 3.1 percent of orders, which would be a jumbo-chunk risk on
customer_idalone, and theorder_datesuffix removes it because a heavy customer's orders split by date. It is not monotonic, because a given day's inserts are spread across whichever customers happen to order. And it targets the query that is 62 percent of our traffic, which is the one that matters.What I am accepting: the 9 percent reporting queries on date alone will still broadcast. That is the correct trade, because they are analytical, they are not latency-sensitive, and I would rather move them to a secondary or an analytics store than distort the shard key for 9 percent of queries at the expense of 62 percent.
What would change my mind: if the top customer were 40 percent rather than 3 percent, the prefix would be too skewed even with the date suffix, and I would consider a synthetic prefix, hashing
customer_idand accepting the loss of the customer-range query.
The _id lookups. 21 percent of queries look up a single order by order_id and
that no longer routes. Two options: include customer_id in the lookup at the API
level, since the caller nearly always has it, which converts a broadcast into a
targeted query; or accept a broadcast on a point lookup, which is cheap because each
shard's index probe is fast. I would do the first, because it is an API change rather
than a database change, and it is worth saying that out loud because it demonstrates
you know the shard key is not the only lever.
Migration: reshardCollection, needing roughly 400 GB of free space across the
cluster, running for hours, with a short write-blocking cutover scheduled in the
maintenance window. Test it on a restored copy first, and measure the actual cutover
duration there.
Production evidence
MongoDB's own documentation names monotonically increasing shard keys as the primary anti-pattern and recommends hashed sharding or a compound key with a non-monotonic prefix, which is unusually direct for vendor documentation and reflects how often it happens.
MongoDB 5.0's reshardCollection exists because wrong shard keys were the most
consequential and least reversible schema decision in the product; MongoDB 8.0 reduced
the cost further by avoiding full duplication in some cases. The feature history is
evidence for how common the mistake is.
The jumbo chunk mechanism is documented behaviour: a chunk containing a single shard-key value cannot be split, so it grows past the chunk size and the balancer refuses to move it. This is the concrete failure mode of a low-frequency-spread key.
Zone sharding is the documented mechanism behind data-residency deployments, and the constraint that the residency field must prefix the shard key is the reason compliance requirements shape the key rather than the other way round.
MongoDB 4.4's compound hashed index support (one hashed field, and only in the
prefix position) exists specifically to enable the {device_id: "hashed", ts: 1}
time-series pattern, which is direct evidence that the compound approach is the
intended answer to the monotonicity/locality tension.
The debate
The case for hashed keys: distribution is guaranteed, there is nothing to reason about, and it removes the entire class of hot-shard incidents. For a write-heavy workload dominated by point lookups (an event store, a session store, a key-value collection), it is simply correct and anything else is over-thinking.
The case for compound ranged keys: they preserve locality, which is what makes range queries and per-tenant queries targeted instead of broadcast. Most real applications are dominated by "everything for this entity" queries, and a hashed key turns every one of those into a scatter-gather.
My position: choose the key from the actual query profile, not from the data model, and default to a compound key whose prefix appears in the dominant query and whose suffix breaks up any skew. Concretely: pull the top five queries by count from the profiler, check which of them name a candidate prefix, and pick the key that routes the largest share of real traffic while passing all three properties. Use hashed only when no such prefix exists, or when the workload genuinely is point lookups and inserts.
Two commitments beyond that. Never shard on a monotonic field, hashed or compound
excepted, because it converts an N-shard cluster into a one-shard cluster for writes
and it is the single most common failure. And check frequency with real data, not
intuition: run a $group over the candidate prefix and look at the top 20 values. If
one value exceeds a few percent of the collection, the key needs a suffix or it needs
hashing.
And I would say plainly that resharding exists now, so this is a recoverable mistake, but that it costs the size of the collection in free space and hours of runtime, which is a reason to spend an afternoon on the analysis rather than a reason to skip it.
Follow-up Q&A
"Pick a shard key for this workload and defend it." I would start from the query
profile rather than the schema: the top five queries by count, and their share. Then
check each candidate against cardinality, frequency and monotonicity. For an order
service where 62 percent of queries are "this customer's recent orders", I would take
{customer_id: 1, order_date: 1}: 4.2 million distinct customers gives cardinality,
the date suffix breaks up any heavy customer so no chunk goes jumbo, it is not
monotonic so writes spread, and it targets the dominant query. I would name what I am
giving up, which is that date-only reporting queries still broadcast, and say why
that is the right trade.
"Why not just hash everything?" Because hashing destroys range and prefix
locality. {_id: "hashed"} gives perfect write distribution and turns the 62 percent
of queries that ask for one customer's orders into a broadcast across every shard, with
a merge and sort at mongos. Hashing is right when the workload really is point
lookups and inserts, like an event store or session store. It is wrong when the
dominant query is "everything for this entity".
"What actually goes wrong with a monotonic key?" Every new document has the highest
value, so it lands in the chunk covering the top of the range, which lives on exactly
one shard. You get an N-shard cluster with single-shard write throughput, and the
symptom is one shard at 70 percent CPU while the rest idle. It is the most common
mistake because _id looks like the obvious choice and ObjectId is monotonic in its
leading timestamp bytes. The fixes are hashing, or a compound key with a
non-monotonic prefix.
"What is a jumbo chunk?" A chunk that has grown past the chunk size but cannot be split, because every document in it shares one shard-key value and MongoDB cannot split below a single value. The balancer then refuses to move it, so it sits on one shard and grows. It is the concrete failure mode of a key with poor frequency spread: one enterprise customer at 40 percent of the collection produces exactly this. The fix is a suffix field that gives the key more resolution.
"You got it wrong. Now what?" Since MongoDB 5.0, reshardCollection, which clones
into the new distribution while applying ongoing writes and then cuts over. The
constraints to state honestly: it needs roughly the collection's size in free space
across the cluster, it runs for hours to days on a large collection, and there is a
short write-blocking window at cutover. MongoDB 8.0 reduced the duplication cost for
some cases. I would rehearse it on a restored copy first to measure the real cutover
duration rather than quoting the documentation.
"How would you know the current key is bad without an incident?"
getShardDistribution for imbalance in chunks, size and document count. Then run
explain on the top five queries and count how many report multiple shards, because
targeted-versus-broadcast is the metric that actually predicts scaling. Then
db.chunks.find({jumbo: true}) in the config database. And a $group on the candidate
prefix to see whether the top 20 values dominate. Those four checks take an hour and
they tell you everything.
"Does data residency change the answer?" Yes, and it constrains rather than
informs. Zone sharding pins ranges to shards in a region, and the zone ranges are
expressed in shard-key space, so the residency field has to be the shard key prefix.
That means {region: 1, customer_id: 1} even if customer_id alone would have been
the better performance choice, because a compliance requirement is not a trade you get
to make.
Common misconceptions
"The shard key is just an index." An index decides speed on one shard; the shard key decides how many shards are involved at all. A broadcast query with a perfect index is still a broadcast query.
"You can change it later, so it does not matter much." Resharding exists and costs the collection's size in free space plus hours of runtime plus a write-blocking cutover. Recoverable is not the same as cheap.
"Hashed is the safe default." Safe for writes, and it converts every range and per-entity query into a scatter-gather. Safety on one axis only.
"High cardinality is enough." Cardinality, frequency and monotonicity are three
separate tests and a key can pass one while failing the others. _id has perfect
cardinality and is the worst common choice.
"A compound shard key targets any query on any of its fields." Only queries containing the prefix route. A query on the suffix alone broadcasts, exactly like a compound index.
Interview delivery note
Start from the queries, not the schema, because that is the reframe that signals experience: "Before I pick a key I'd want the profiler output: the top five queries by count and their share. The shard key's job is to make the dominant query targeted instead of broadcast, and I can't choose it from the data model alone."
Then run the three tests out loud, since that is the defence being asked for: "Cardinality, frequency, monotonicity. Cardinality is 4.2 million customers, so we can split as far as we need. Frequency: the top customer is 3.1 percent, which would be a jumbo-chunk risk on customer_id alone, and adding order_date as a suffix breaks that up because their orders now split by date. And it's not monotonic, so a day's inserts spread across whichever customers happen to order."
Then name the trade explicitly, because a defence that claims no downside is not a defence: "What I'm accepting is that the nine percent of reporting queries on date alone still broadcast. I'd take that, because they're analytical rather than latency-sensitive, and I'd move them to a secondary or an analytics store rather than distort the key for nine percent of traffic at the expense of sixty-two."
The line that most often lands: "and the answer that fails is _id, because ObjectId
is monotonic in its leading timestamp bytes, so every insert goes to the shard holding
the top chunk and you have a six-shard cluster with one shard's write throughput."
Further reading
- MongoDB manual, "Choose a Shard Key", and "Shard Key Selection" including the cardinality, frequency and monotonicity discussion.
- MongoDB manual, "Reshard a Collection", for the resource requirements and the cutover behaviour.
- MongoDB manual, "Zones", for residency-constrained sharding.
- MongoDB manual, "Hashed Sharding" and the 4.4 compound hashed index notes, for the
{device_id: "hashed", ts: 1}time-series pattern.