UUID vs MongoDB ObjectId
On this page
TL;DR. MongoDB’s
ObjectIdis a 12-byte time-ordered ID specific to MongoDB. UUID v7 is a 16-byte time-ordered ID that’s a universal standard. Use ObjectId in pure-MongoDB systems for compactness; use UUID v7 anywhere else, including MongoDB if you cross system boundaries.
What ObjectId is
MongoDB’s ObjectId is a 12-byte (96-bit) value generated by MongoDB drivers. Layout:
┌────────────────┬──────────────────┬─────────────────┐
│ 4-byte │ 5-byte │ 3-byte │
│ Unix-second │ random per- │ incrementing │
│ timestamp │ process value │ counter │
└────────────────┴──────────────────┴─────────────────┘
- 4 bytes of timestamp (seconds since Unix epoch)
- 5 bytes of randomness, generated once per process
- 3 bytes of counter, incrementing within the process
It’s MongoDB’s default _id for any collection where you don’t specify one. It’s been there since MongoDB 1.0 (2009).
Side-by-side
| UUID v7 | MongoDB ObjectId | |
|---|---|---|
| Bytes | 16 | 12 |
| Hex string length | 36 (with hyphens) | 24 |
| Timestamp resolution | 1 millisecond | 1 second |
| Timestamp bits | 48 | 32 |
| Random bits | 74 | 40 (per-process, 24 of which is a counter) |
| Universal standard | RFC 9562 (IETF) | MongoDB-specific |
| Coordination | none | none |
| Sortable | yes (lexicographic) | yes (lexicographic) |
| Native in non-MongoDB DBs | yes | no |
Where ObjectId wins
- 4 bytes smaller. On a billion-document collection, that’s a 4 GB difference in
_idindex size, plus every reference field. - Shorter string form. 24 chars vs 36 — slightly more compact in URLs and logs.
- Native in MongoDB. No conversion overhead, no driver gymnastics. Every MongoDB tool understands ObjectId without configuration.
- Counter for same-second uniqueness. Even at 1M+ inserts/sec from a single process, ObjectIds remain strictly monotonic.
Where UUID v7 wins
- Universal standard. UUIDs work in every database, every language, every framework. ObjectIds outside MongoDB are a custom type.
- Cross-system identity. A UUID generated by your Mongo write is the same value when published to Kafka, indexed in Elasticsearch, returned to a web client. ObjectIds need to be converted at every boundary.
- Better timestamp resolution. v7 has milliseconds; ObjectId has seconds. For high-throughput systems where second-level resolution loses ordering between many docs, v7 keeps them apart.
- Cryptographic randomness. v7 has 74 random bits via CSPRNG. ObjectId’s 5 random bytes are generated once per process (then a counter increments) — fine for uniqueness, but predictable within a process boundary in a way v7’s random bytes are not. Don’t use ObjectIds as security tokens.
- Stable across MongoDB-and-other-stores migrations. If you might one day move data out of MongoDB (Postgres, ClickHouse, S3 Parquet), UUIDs travel cleanly. ObjectIds need a conversion table.
When to use which
Single-system MongoDB, no integrations, you control all clients
└── ObjectId is fine — that's what it's for
MongoDB + other databases / message queues / external systems
└── UUID v7 (consistency at boundaries beats per-DB compactness)
Public-facing IDs (URLs, share tokens)
└── UUID v7 (the standard is recognizable; ObjectIds look like a leak of internal infra)
Greenfield project, MongoDB is one of multiple data stores
└── UUID v7 stored as BinData — same on-disk size as ObjectId in MongoDB,
universal everywhere else
Storing UUIDs in MongoDB
MongoDB has a native BinData type for binary fields. UUIDs are 16 bytes — store them as BinData(4) (subtype 4 = UUID per RFC 4122):
import { Binary } from "mongodb";
import { v7 } from "uuid";
const id = v7(); // canonical string
const bytes = Buffer.from(id.replace(/-/g, ""), "hex");
const binId = new Binary(bytes, Binary.SUBTYPE_UUID); // BinData(4, ...)
await collection.insertOne({ _id: binId, email: "alice@example.com" });
Or use bson.UUID if your driver supports it (Node’s official MongoDB driver does in 4.x+):
import { UUID } from "mongodb";
await collection.insertOne({ _id: new UUID(id), email });
This stores 16 bytes — same wire size as the 12-byte ObjectId in disk page bytes (the difference is amortized by row overhead). Indexes are slightly larger but typically not a bottleneck.
Code: timestamp from each
ObjectId timestamp
import { ObjectId } from "mongodb";
const id = new ObjectId();
id.getTimestamp();
// 2026-04-26T12:34:56.000Z (second precision)
UUID v7 timestamp
function v7Timestamp(uuid) {
const hex = uuid.replace(/-/g, "").slice(0, 12);
return new Date(Number(BigInt("0x" + hex)));
}
v7Timestamp("01928a47-3b30-7c5e-9d1a-f0b8c4a7e923");
// 2024-10-09T21:32:30.123Z (millisecond precision)
Both let you recover when a record was created. Useful for forensics, audit, debugging.
Migration: ObjectId → UUID v7
If you decide to migrate a MongoDB collection from ObjectId to UUID:
const cursor = collection.find({});
for await (const doc of cursor) {
const newId = new UUID(uuidv7());
await collection.insertOne({ ...doc, _id: newId, legacy_id: doc._id });
// update foreign references in dependent collections, then:
await collection.deleteOne({ _id: doc._id });
}
In practice, don’t backfill. Add UUIDs alongside the existing ObjectId for new documents, and update foreign-reference fields to use the UUID going forward. Mixed-mode is fine — the version (UUID v7 vs ObjectId) is detectable from the bytes.
Common pitfalls
new ObjectId(string)accepts both 24-char hex and UUID strings — don’t pass a UUID accidentally; it’ll succeed at runtime but produce nonsense.- Don’t compare ObjectIds with
==. Use.equals()— they’re objects. - ObjectId timestamps are seconds, not milliseconds. Two ObjectIds from the same second within the same process are ordered by counter; across processes, ordering within a second is undefined.
- MongoDB Atlas Search and aggregation pipelines treat ObjectId as a first-class type; UUID-as-BinData requires explicit handling in some operators.
Decision flowchart
Is MongoDB the only database in your stack?
├── Yes → ObjectId (you're already there)
└── No
├── Do you cross system boundaries with these IDs?
│ ├── Yes → UUID v7
│ └── No → ObjectId
└── Are these IDs ever public-facing?
└── Yes → UUID v7 (don't expose internal infra)
Are you greenfield with no preference?
└── UUID v7 (universal beats specialized for new systems in 2026)
Try the tools
- UUID v7 generator — works as MongoDB
_id - UUID validator — paste a UUID, decode the timestamp
- UUID as primary key — design discussion
- UUID vs Snowflake — for the 64-bit alternative