UUID vs MongoDB ObjectId

An ObjectId is 12 bytes against a UUID's 16 — but it spends only 40 bits on randomness, fixed per process, where a v4 spends 122 fresh every time.

What each one is made of

ObjectId — 12 bytes, 24 hex characters
┌────────────┬──────────────┬──────────┐
│ timestamp  │ random       │ counter  │
│ 4 B, sec   │ 5 B/process  │ 3 B      │
└────────────┴──────────────┴──────────┘

UUID v7 — 16 bytes, 36 characters
┌──────────────────┬─────────────────────┐
│ unix_ts_ms 48 b  │ version + 74 random │
└──────────────────┴─────────────────────┘

Both solve the same problem — unique identifiers minted anywhere without asking a central authority — and both put a timestamp first so values cluster on insert. They differ in how many bits they are willing to spend on it.

Side by side

ObjectIdUUID v4UUID v7
Stored size12 bytes16 bytes
Displayed24 chars36 chars
Time resolution1 secondnone1 millisecond
Random bits40, fixed per process12274 per ms
Sorts by creation✅ to the second❌✅ to the ms
Timestamp range32-bit — ends 2106—48-bit — far beyond

The randomness row is the one that matters most and gets the least attention. An ObjectId's 5-byte random segment is generated once per process and reused for every identifier that process creates. Anyone holding two ObjectIds from the same process knows that segment, leaving only a timestamp and a counter to guess — a vastly smaller space than 122 independent random bits.

The timestamp width is the other quiet difference. An ObjectId spends four bytes on seconds since the epoch, which is a signed 32-bit range and therefore runs out in 2106 — distant, but the same shape of problem as every other 32-bit time field. A v7 spends six bytes on milliseconds and does not have that horizon at all.

Storing a UUID in MongoDB

The _id field takes any BSON type, so a UUID is a perfectly ordinary primary key. Store it as Binary subtype 4, the UUID subtype, and it occupies the same 16 bytes it would anywhere else. A 36-character string costs more than double and turns every index comparison into a string comparison.

There is history to be careful of. Older drivers used subtype 3 and did not agree on byte order — the .NET driver used one arrangement, Java and Python another — so the same logical UUID stored by two services could compare unequal or sort differently. Subtype 4 mandates RFC byte order and settles it. If you inherit a collection using subtype 3, find out which driver wrote it before migrating; the same trap appears in UUID vs GUID for the .NET case.

Choosing

Stay with ObjectId when the data lives only in MongoDB and identifiers never leave it. It is the native type, four bytes leaner, and every driver and tool understands it without configuration.

Reach for UUID v7 when identifiers cross a boundary — into another datastore, an API contract, a client that creates records offline. A UUID means the same thing in PostgreSQL, in JSON and in a URL, where an ObjectId is a MongoDB concept that other systems store as an opaque string. Use v4 instead when the creation time itself is sensitive, which neither ObjectId nor v7 can hide — see is a UUID secure.

Frequently asked questions

What is inside a MongoDB ObjectId?

Twelve bytes in three parts: a 4-byte timestamp in whole seconds since the Unix epoch, a 5-byte value random per process, and a 3-byte counter that increments within that process. The design targets uniqueness across machines without coordination, the same goal as a UUID, but it spends far fewer bits doing it. The counter is what prevents collisions inside a single second, and three bytes allows 16,777,216 distinct values per second per process before it wraps. The timestamp being seconds rather than milliseconds is why ObjectIds created in the same second sort only by counter, which orders correctly within a process and not at all between them, which matters once you scale past a single writer.

Is an ObjectId smaller than a UUID?

Yes, by four bytes stored and twelve characters displayed. An ObjectId occupies 12 bytes against a UUID's 16, and renders as 24 hex characters against 36 with hyphens or 32 without. In a collection with several indexes that difference is paid per index entry, so it is not nothing, but it is a quarter rather than an order of magnitude. The larger practical difference is the randomness each spends its bytes on: an ObjectId has 40 random bits fixed per process plus a counter, while a UUID v4 has 122 random bits regenerated every time. Four bytes of storage is a real saving at scale; forty bits of shared randomness is a real cost in guessability.

Can I use a UUID as the _id in MongoDB?

Yes. The _id field accepts any BSON type, so a UUID is a legitimate primary key and MongoDB will not object. Store it as BSON Binary subtype 4, the UUID subtype, rather than as a 36-character string: binary keeps it to 16 bytes where the string form costs 36 and turns every comparison into a string comparison. Be aware that older drivers used subtype 3 with differing byte orders across languages, which produced values that looked identical in one driver and sorted differently in another. Subtype 4 fixed that by mandating RFC byte order, so check which subtype an inherited collection uses before assuming its values compare correctly across services.

Which one sorts by creation time?

Both, but at different resolutions. An ObjectId leads with a 4-byte timestamp in whole seconds, so sorting by _id gives creation order down to the second and then falls back to the per-process counter, which does not order across processes. A UUID v7 leads with a 48-bit millisecond timestamp, giving a thousand times finer ordering and no cross-process ambiguity beyond the millisecond. A UUID v4 has no timestamp at all and sorts randomly. If insert locality is why you are choosing, ObjectId and v7 both deliver it and v4 does not. The choice between the first two then comes down to whether one-second granularity is fine enough for your write volume.

Does an ObjectId leak more than a UUID?

It leaks creation time, like UUID v7, and it is more guessable than either UUID version. Only 40 of its 96 bits are random and that value is fixed for the lifetime of a process, so an attacker holding two ObjectIds from the same process learns the random segment and needs only to guess a timestamp and a counter. That is a far smaller space than the 122 random bits of a v4. Historically the random segment was derived from the machine identifier and process ID, which disclosed infrastructure detail directly. Neither is suitable as an unguessable public identifier, and neither should be the only thing standing between a stranger and a record.

References

Generate v7 keys with the UUID generator, or read one with the decoder. Related: UUID vs ULID