MongoDB generates ObjectId values by default for the _id field. ObjectId is 12 bytes, time-ordered, and works well in a pure MongoDB stack. But for teams using MongoDB alongside PostgreSQL, MySQL, or any SQL database — or for applications that need to reference the same entity across multiple data stores — ObjectId’s non-standard format creates friction. UUID v7 solves this: it is time-ordered like ObjectId, 16 bytes, and portable across every database and language runtime.
ObjectId vs UUID v7
| Property | ObjectId | UUID v7 |
|---|---|---|
| Size | 12 bytes | 16 bytes |
| Time-ordered | Yes (4-byte Unix second) | Yes (48-bit Unix millisecond) |
| Timestamp precision | 1 second | 1 millisecond |
| Standard | MongoDB-specific | RFC 9562 (IETF) |
| Cross-database portable | No | Yes |
| String representation | 24-char hex | 36-char 8-4-4-4-12 |
| Native BSON type | Yes (ObjectId) | Yes (Binary subtype 4) |
ObjectId encodes a 4-byte Unix timestamp (second precision), a 5-byte random machine identifier, and a 3-byte incrementing counter. UUID v7 uses a 48-bit millisecond timestamp with 74 bits of randomness. Both are time-ordered; UUID v7 has finer timestamp precision and more entropy.
Storing UUIDs in MongoDB with the Node.js driver
The MongoDB Node.js driver’s BSON library includes a UUID class that stores as Binary subtype 4 (the standard BSON UUID type):
import { MongoClient, UUID } from 'mongodb'; // UUID re-exported from bson
import { v7 as uuidv7 } from 'uuid';
const client = new MongoClient(process.env.MONGODB_URI);
const db = client.db('myapp');
const users = db.collection('users');
// Insert with UUID v7 as _id
await users.insertOne({
_id: new UUID(uuidv7()),
email: 'user@example.com',
createdAt: new Date(),
});
// Find by UUID
const user = await users.findOne({ _id: new UUID('019236a7-b4f2-7000-8d3e-9c1a2b3d4e5f') });
The UUID class wraps the 16 raw bytes and serialises to BSON Binary subtype 4. Queries and indexes work on the binary value, so comparisons and sorts use 16 bytes rather than a 36-character string.
Mongoose integration
Mongoose does not have a built-in UUID type, but you can use Buffer with the BSON UUID class:
import mongoose from 'mongoose';
import { UUID } from 'mongodb';
import { v7 as uuidv7 } from 'uuid';
const userSchema = new mongoose.Schema({
_id: {
type: Buffer,
subtype: 4, // BSON Binary subtype 4 = UUID
default: () => new UUID(uuidv7()).buffer,
},
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now },
});
// Virtual to get the UUID string from the buffer
userSchema.virtual('id').get(function () {
return new UUID(this._id).toString();
});
const User = mongoose.model('User', userSchema);
For applications that need a simpler setup, storing the UUID as a plain string is also valid:
const userSchema = new mongoose.Schema({
_id: {
type: String,
default: () => uuidv7(),
},
email: String,
});
String storage trades 16-byte binary efficiency for simplicity. For most Mongoose applications the difference is not significant. Use binary if you are optimising for index size in a high-volume collection.
Indexing behaviour
UUID v7’s time-ordered insert pattern benefits MongoDB’s WiredTiger storage engine in the same way it benefits B-tree indexes in SQL databases. When _id values are monotonically increasing, new documents append to the end of the B-tree rather than inserting at random positions. This reduces page splits and keeps the working set in the buffer cache.
ObjectId has the same property at second precision. UUID v7 improves on this with millisecond precision and a per-millisecond monotonic counter, ensuring that all documents inserted within the same second arrive in generation order.
Migration from ObjectId
If you have an existing collection using ObjectId _id values, you cannot change the type of the existing field — MongoDB _id is immutable. The migration path is:
- Add a new
uuidfield to all documents with UUID v7 values - Create a unique index on
uuid - Update application code to use
uuidas the public identifier - Optionally keep
_idas ObjectId for internal MongoDB operations
// Step 1: back-fill uuid field for existing documents
const { v7: uuidv7 } = await import('uuid');
const cursor = db.collection('users').find({}, { projection: { _id: 1 } });
for await (const doc of cursor) {
await db.collection('users').updateOne(
{ _id: doc._id },
{ $set: { uuid: uuidv7() } }
);
}
// Step 2: create unique index
await db.collection('users').createIndex({ uuid: 1 }, { unique: true });
For new collections, use UUID v7 as _id from the start and avoid the migration entirely.
Atlas Search and UUID
If you use MongoDB Atlas Search, note that UUID binary fields are not directly searchable as text — you need to store a string representation alongside or instead of the binary. For search-heavy use cases, store the UUID as a plain string field and accept the minor storage overhead.
Further reading
- UUID in Databases — storage across PostgreSQL, MySQL, SQLite, and MongoDB
- UUID v7 in Distributed Systems — multi-node ID generation and event ordering
- UUID Primary Keys vs Auto-Increment — when to use UUIDs vs sequential integers
Frequently asked questions
Should I use UUID or ObjectId in MongoDB?
For pure MongoDB stacks, ObjectId is simpler and time-ordered. For architectures spanning multiple databases or exposing IDs to external systems, UUID v7 is better — it is an RFC standard portable across every database and language runtime. Both are time-ordered; UUID v7 has millisecond precision versus ObjectId's second precision.
What column type should I use to store a UUID in a database?
Use the native type where one exists — uuid in PostgreSQL, uniqueidentifier in SQL Server. In MySQL use BINARY(16) and store raw bytes. Avoid VARCHAR(36): it costs 36 bytes per row versus 16 for binary and makes every comparison a string operation.
Can UUID v7 replace a central sequence counter in distributed systems?
For most use cases, yes. UUID v7's millisecond timestamp gives approximate cross-node ordering without any coordination. Each node generates independently, and UUIDs sort in creation order to millisecond precision. For strict global ordering you still need clock synchronisation or a coordination layer — but UUID v7 handles the 99% case where millisecond-level ordering is sufficient.
Is UUID v7 better than ObjectId in MongoDB?
For teams using MongoDB alongside relational databases, UUID v7 is better — it is a single portable identifier format across all data stores. For pure MongoDB deployments, ObjectId is simpler (no setup needed) and has similar time-ordering properties. UUID v7 is the right choice when your architecture spans multiple database types or when you expose IDs to external systems that expect UUID format.
Can MongoDB store UUIDs natively?
Yes. MongoDB's BSON format has a native Binary subtype 4 (UUID) that stores 16 bytes. You can define UUID fields as Binary subtype 4 in your schema. Mongoose supports this via the mongoose-uuid package or by using Buffer type with a custom getter/setter. The MongoDB Node.js driver's BSON library includes a UUID class directly.