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 UUIDv4 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 UUIDv4 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, UUIDv7) 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 under sharding:

  • Range sharding on _id concentrates recent inserts onto the chunk that owns the highest key values → write hotspot.
  • Hashed sharding on _id distributes 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 UUIDv7 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 UUIDv7 / ULID 16 B Excellent Yes High Best UUID option
Binary UUIDv4 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(...),    // UUIDv7 exposed to clients and other systems
  // ...
}

You keep MongoDB’s best index behavior while still offering a portable identifier.

Recommendations

  1. Prefer the default ObjectId unless you have a concrete requirement that it cannot satisfy.
  2. If you need a UUID, use a time-ordered variant (UUIDv7 or ULID) and store it as BSON Binary subtype 4.
  3. Never store UUIDs as strings.
  4. For public APIs consider the hybrid pattern: ObjectId as _id, UUIDv7 as a separate unique field.
  5. When sharding, avoid a pure monotonic shard key; use hashed sharding or a high-cardinality compound key that includes a random or hashed component.
  6. Measure index size (db.collection.stats().indexSizes) and insert latency at your expected scale before committing to a custom _id strategy.

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 UUIDv4 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.


Tools and Versions

Need some UUIDs for testing? Try our tools for generating UUIDv4   UUIDv7   ULID   NanoID and decoding validating.
For a detailed comparison of UUID versions, visit our Complete Guide to UUID Versions.