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 _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 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

  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 (UUID v7 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, UUID v7 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 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.