Published 2026-07-22 | Last Updated: 2026-08-03
UUID Primary Keys in MongoDB: Do You Even Need Them?
MongoDB requires every document to have a unique _id field that serves as the primary key. By default the server (or driver) generates a 12-byte ObjectId. Many teams replace it with a UUID, usually for portability, client-side generation, or to avoid MongoDB-specific types. The question is whether that substitution is worth the cost.
This post examines the storage-engine mechanics, quantifies the performance differences, and provides clear guidance on when a UUID is justified and how to use one correctly if you must.
How _id and Indexes Work in WiredTiger
MongoDB’s default storage engine, WiredTiger, stores collection data in a B-tree (or columnar for some workloads) and maintains a separate unique B-tree index on _id. The index maps the _id value to an internal RecordId that points to the document.
Because the _id index is mandatory and unique, every insert performs an index insertion. The shape of the key therefore determines:
- How often leaf pages split
- The average fill factor of those pages
- How much of the index fits in the WiredTiger cache
- The amount of write amplification and journal traffic
ObjectId Anatomy
An ObjectId is 12 bytes:
- 4 bytes - Unix timestamp (seconds)
- 5 bytes - random value (machine + process entropy in modern drivers)
- 3 bytes - incrementing counter
The leading timestamp makes successive ObjectIds roughly monotonic. Inserts therefore tend to hit the right-hand side of the B-tree, producing high fill factors and good cache locality.
UUID Characteristics
A standard UUID is 16 bytes. When stored as a BSON Binary subtype 4 it occupies 16 bytes of key data plus a small type tag. Stored as a string it balloons to 36 bytes of UTF-8 plus overhead-almost never acceptable.
Random UUID v4 values are uniformly distributed. Each insert lands at a random leaf, causing frequent page splits, lower fill factors, and a larger working set that is harder to keep in cache.
Measured Impact
Independent benchmarks (MongoDB 6.x, modern drivers) consistently show the following pattern:
| Scenario | ObjectId | Binary UUID v4 | Relative Cost of UUID |
|---|---|---|---|
| 1 M batched inserts (empty collection) | Baseline | 50-55 % slower | Significant |
| 10 M inserts into collection already holding 10 M docs | Baseline | ~2× slower | Severe |
| Index size at 20 M documents | ~192 MiB | ~750 MiB | ~4× larger |
Point lookup by _id (warm cache) |
~150 µs | ~165-170 µs | Modest |
| Bulk insert throughput at scale | Highest | 2-3.5× lower | High |
Time-ordered alternatives (ULID, UUID v7) close most of the gap and produce index sizes closer to ObjectId.
Secondary indexes are not inflated by the size of _id in WiredTiger (they store RecordIds, not the primary key value). The penalty is confined to the mandatory _id index itself, the document size, and any application-level references that also store the identifier.
Sharding Considerations
ObjectId’s monotonicity is a double-edged sword when it comes to sharding:
- Range sharding on
_idconcentrates recent inserts onto the chunk that owns the highest key values → write hotspot. - Hashed sharding on
_iddistributes writes evenly but destroys the locality benefits of the ordered index.
A pure random UUID distributes writes naturally but still pays the random-index penalty on every shard. A time-ordered UUID or a carefully chosen compound shard key is usually superior.
When a UUID Makes Sense
Use a UUID (preferably time-ordered) only when one or more of the following is true:
- Identifiers must be generated by clients, edge devices, or multiple independent services before the document reaches MongoDB.
- The same identifier must be shared across multiple databases or message buses that do not understand ObjectId.
- You need an opaque, non-guessable public identifier and do not want to expose ObjectId timestamps.
- You are building a polyglot or multi-tenant system where MongoDB is only one of several stores.
In all other cases ObjectId remains the better default: smaller, faster, natively supported, and already globally unique for practical purposes.
Correct Ways to Store a UUID
Never store a UUID as a string.
// Bad - 36-byte string
{ _id: "550e8400-e29b-41d4-a716-446655440000" }
// Good - BSON Binary subtype 4 (16 bytes)
{ _id: UUID("550e8400-e29b-41d4-a716-446655440000") }
In the Node.js driver:
const { Binary } = require('mongodb');
const { v7: uuidv7 } = require('uuid');
const id = uuidv7();
const binaryId = new Binary(Buffer.from(id.replace(/-/g, ''), 'hex'), 4);
await collection.insertOne({ _id: binaryId, ...doc });
In C# with the official driver:
using MongoDB.Bson;
using MongoDB.Driver;
var id = Guid.NewGuid(); // or a UUID v7 library
var doc = new BsonDocument {
{ "_id", new BsonBinaryData(id, GuidRepresentation.Standard) },
// ...
};
await collection.InsertOneAsync(doc);
Practical Alternatives
| Approach | Size | Index Locality | Client Generation | Cross-system Portability | Recommendation |
|---|---|---|---|---|---|
| Default ObjectId | 12 B | Excellent | Driver/server | MongoDB-centric | Default choice |
| Binary UUID v7 / ULID | 16 B | Excellent | Yes | High | Best UUID option |
| Binary UUID v4 | 16 B | Poor | Yes | High | Avoid for large collections |
| String UUID | 36 B | Poor | Yes | High | Never |
Hybrid (ObjectId _id + public UUID) |
12 B + 16 B | Excellent | Yes | High | Often ideal |
| Natural key (email, external ID) | Varies | Depends | Yes | High | When truly immutable |
The hybrid pattern is frequently the cleanest compromise:
{
_id: ObjectId(), // internal, optimal index
publicId: Binary(...), // UUID v7 exposed to clients and other systems
// ...
}
You keep MongoDB’s best index behavior while still offering a portable identifier.
Recommendations
- Prefer the default
ObjectIdunless you have a concrete requirement that it cannot satisfy. - If you need a UUID, use a time-ordered variant (UUID v7 or ULID) and store it as BSON Binary subtype 4.
- Never store UUIDs as strings.
- For public APIs consider the hybrid pattern: ObjectId as
_id, UUID v7 as a separate unique field. - When sharding, avoid a pure monotonic shard key; use hashed sharding or a high-cardinality compound key that includes a random or hashed component.
- Measure index size (
db.collection.stats().indexSizes) and insert latency at your expected scale before committing to a custom_idstrategy.
Conclusion
You rarely need a UUID as the MongoDB _id. The native ObjectId is smaller, produces denser indexes, inserts faster at scale, and already provides distributed uniqueness. Random UUID v4 values re-introduce the classic random-key fragmentation problems that B-trees suffer from, while offering little advantage inside a pure MongoDB workload.
When portability or client-side generation is mandatory, reach for a time-ordered UUID stored in binary form, or adopt the hybrid ObjectId-plus-public-UUID pattern. In the majority of applications the simplest answer remains the best: let MongoDB generate the ObjectId and move on.
Related Tools and Guides
Need some UUIDs for testing? Try our tools for generating
UUID v4
UUID v7
ULID
NanoID
and
decoding
validating.
For a detailed comparison of UUID versions, visit our Complete Guide to UUID Versions.