MongoDB ObjectID Explained: Structure, Timestamp, and When to Use It

10 March, 2026 Backend

What Is ObjectID

Every document stored in MongoDB needs a unique identifier. By default, MongoDB assigns a field named _id to each document, and unless you provide your own value, MongoDB generates an ObjectID automatically.

An ObjectID appears as a 24-character hexadecimal string:

6619a3f2e4b0c1d9f8a72e30

Under the hood, those 24 hex characters represent 12 bytes of binary data. It is defined as a BSON type called ObjectId and is specific to the BSON specification used by MongoDB. It is not a UUID - it has a different structure, different size, and different guarantees than RFC 4122 UUIDs.

ObjectIDs are designed to be generated client-side (in the driver, before the document is sent to the server), which allows drivers to construct the _id field without a round-trip to MongoDB. This is one of the reasons MongoDB can scale writes efficiently across distributed environments.


The 12-Byte Structure

The 12 bytes of an ObjectID are divided into three distinct components:

|<-- 4 bytes -->|<----- 5 bytes ----->|<-- 3 bytes -->|
+---------------+---------------------+---------------+
|  Unix timestamp  |   Random value    |   Counter     |
|  (seconds)       |   (per process)   |   (24-bit)    |
+---------------+---------------------+---------------+
  Bytes 0-3          Bytes 4-8           Bytes 9-11

Bytes 0-3: Unix Timestamp (4 bytes)

The first four bytes store a Unix timestamp at second precision (not millisecond). This is the number of seconds elapsed since the Unix epoch (January 1, 1970, 00:00:00 UTC), encoded as a big-endian unsigned 32-bit integer.

Because the timestamp occupies the most significant bytes and is big-endian, ObjectIDs are roughly sortable by creation time - older IDs sort before newer ones.

The 4-byte timestamp will overflow on February 7, 2106 (Unix timestamp 2^32 = 4294967296). Not a concern for most applications today, but worth knowing.

Bytes 4-8: Random Value (5 bytes)

The next five bytes contain a random value that is generated once per process when the MongoDB driver initialises. This value is unique per machine and process combination.

Important historical note: Before MongoDB 4.0, these 5 bytes were split differently - 3 bytes for a machine identifier (derived from the hostname) and 2 bytes for the process ID. This older scheme leaked information about the host machine. Since MongoDB 4.0, drivers replaced this with a single random value generated at startup, which is more privacy-preserving.

If you are working with ObjectIDs generated by older MongoDB clients (pre-4.0 drivers), be aware that bytes 4-8 may contain identifiable machine and process information.

Bytes 9-11: Incrementing Counter (3 bytes)

The final three bytes are a 24-bit incrementing counter. It is initialised to a random value when the process starts (not zero), and increments by one for each ObjectID generated within the same second by the same process.

This means within a single second, a single process can generate up to 2^24 = 16,777,216 unique ObjectIDs before the counter wraps around.

Putting It Together

Given the ObjectID 6619a3f2e4b0c1d9f8a72e30:

66 19 a3 f2  |  e4 b0 c1 d9 f8  |  a7 2e 30
^timestamp^     ^random value^     ^counter^
  • 0x6619a3f2 = 1712972786 in decimal = Unix timestamp for approximately 2024-04-13 06:26:26 UTC
  • 0xe4b0c1d9f8 = random 5-byte value unique to the generating process
  • 0xa72e30 = counter value at the time of generation

Extracting the Timestamp

One practical consequence of the embedded timestamp is that you can determine when a document was created without storing a separate createdAt field and without querying the database.

The algorithm is simple:

  1. Take the first 8 hex characters of the ObjectID string (representing 4 bytes)
  2. Parse those 8 hex characters as a big-endian unsigned 32-bit integer
  3. The result is the Unix timestamp in seconds

PHP

function objectIdToTimestamp(string $objectId): int
{
    return hexdec(substr($objectId, 0, 8));
}

function objectIdToDateTime(string $objectId): DateTimeImmutable
{
    $timestamp = objectIdToTimestamp($objectId);
    return new DateTimeImmutable('@' . $timestamp);
}

// Usage
$id = '6619a3f2e4b0c1d9f8a72e30';
$dt = objectIdToDateTime($id);
echo $dt->format('Y-m-d H:i:s'); // 2024-04-13 06:26:26

// Using the official mongodb extension
$objectId = new MongoDB\BSON\ObjectId('6619a3f2e4b0c1d9f8a72e30');
$timestamp = $objectId->getTimestamp(); // Returns Unix timestamp as integer
$dt = new DateTimeImmutable('@' . $timestamp);

Python

from bson import ObjectId
from datetime import datetime, timezone

def objectid_to_timestamp(object_id_str: str) -> int:
    return int(object_id_str[:8], 16)

def objectid_to_datetime(object_id_str: str) -> datetime:
    ts = objectid_to_timestamp(object_id_str)
    return datetime.fromtimestamp(ts, tz=timezone.utc)

# Manual extraction
oid_str = "6619a3f2e4b0c1d9f8a72e30"
print(objectid_to_datetime(oid_str))  # 2024-04-13 06:26:26+00:00

# Using pymongo's built-in method
oid = ObjectId("6619a3f2e4b0c1d9f8a72e30")
print(oid.generation_time)  # datetime.datetime(2024, 4, 13, 6, 26, 26, tzinfo=<UTC>)

JavaScript

// Using the official mongodb driver
const { ObjectId } = require('mongodb');

const oid = new ObjectId('6619a3f2e4b0c1d9f8a72e30');

// Built-in method - returns a Date object
const date = oid.getTimestamp();
console.log(date.toISOString()); // 2024-04-13T06:26:26.000Z

// Manual extraction
function objectIdToTimestamp(objectIdStr) {
    return parseInt(objectIdStr.substring(0, 8), 16);
}

function objectIdToDate(objectIdStr) {
    return new Date(objectIdToTimestamp(objectIdStr) * 1000);
}

console.log(objectIdToDate('6619a3f2e4b0c1d9f8a72e30').toISOString());

Note that the timestamp has second precision, not millisecond. The Date object returned by getTimestamp() will always have 000 milliseconds.


ObjectID vs UUID

Developers often ask whether to use MongoDB's ObjectID or a standard UUID. Here is a direct comparison:

Property ObjectID UUIDv4 UUIDv7
Size (bytes) 12 bytes 16 bytes 16 bytes
Hex string length 24 characters 36 characters (with dashes) 36 characters (with dashes)
Byte representation BSON ObjectId Binary (RFC 4122) Binary (RFC 9562)
Sortable by creation time Yes (second precision) No (random) Yes (millisecond precision)
Has embedded timestamp Yes (seconds) No Yes (milliseconds)
Timestamp precision 1 second N/A 1 millisecond
Standard BSON specification RFC 4122 RFC 9562
Uniqueness scope Per-process random + counter Global random (statistically) Global random (statistically)
Cross-system compatibility MongoDB-specific Universal Universal
Counter overflow risk Yes (24-bit, wraps at 16M/sec/process) N/A Depends on implementation

Key takeaways:

  • If you are using MongoDB and staying within the MongoDB ecosystem, ObjectID is the natural choice. It is compact (12 bytes vs 16 for UUID), has a built-in timestamp, and is supported natively by all MongoDB drivers.
  • If you need IDs that work across multiple databases or systems, use UUID (preferably UUIDv7 for sortability).
  • UUIDv7 has millisecond timestamp precision vs ObjectID's second precision - UUIDv7 wins on sort fidelity within the same second.
  • UUIDv4 should be avoided when sortability matters; its random nature leads to B-tree fragmentation in indexed columns.

Sorting by ObjectID

Because the Unix timestamp occupies the first 4 bytes in big-endian order, ObjectIDs are K-sortable - sorting them lexicographically produces an approximately chronological order.

This has practical implications:

// These ObjectIDs will sort in creation order
const ids = [
    new ObjectId('6619a3f2e4b0c1d9f8a72e30'), // older
    new ObjectId('661a5c10e4b0c1d9f8a72e31'), // newer
    new ObjectId('661b0001e4b0c1d9f8a72e32'), // newest
];

// Sorting works correctly for cross-second ordering
ids.sort((a, b) => a.toString().localeCompare(b.toString()));

Within the same second, the sort order is determined by the random bytes (bytes 4-8) and the counter (bytes 9-11). The random component means that multiple processes generating IDs in the same second will not have a guaranteed order relative to each other - only within a single process is the counter monotonically increasing.

This is a meaningful difference from UUIDv7, which uses millisecond precision in its timestamp component, making it better at preserving insertion order for high-throughput systems where many documents are created within the same second.

For MongoDB specifically, ObjectID's second-precision sortability is usually sufficient. If you are inserting hundreds of thousands of documents per second and need strict ordering, consider adding an explicit monotonic sequence field.


Security Considerations

ObjectIDs are not secret identifiers. They should never be treated as tokens, passwords, or unpredictable values. Here is why:

The Timestamp Is Extractable

Anyone who sees an ObjectID can extract the creation timestamp. This can reveal:

  • When a document was created
  • By inference, business metrics (e.g., how many orders were placed - if you sign up today and get ObjectID ...00001, and tomorrow see ...00500, you know 499 new documents were created)
  • Approximate system load and usage patterns

Pre-4.0 Versions Leaked Machine Identity

ObjectIDs generated by MongoDB drivers before version 4.0 embedded the machine's hostname hash and process ID in bytes 4-8. An attacker with access to a sample ObjectID could potentially identify the server hostname and narrow down the process. This is no longer a concern with modern drivers (4.0+), which use a random value instead.

The Counter Is Guessable

Because the counter increments monotonically and starts from a random value at process start, an attacker who observes two consecutive ObjectIDs can determine the counter increment and predict future IDs.

What This Means in Practice

  • Do NOT use ObjectID as an authentication token (password reset link, session token, email verification link)
  • Do NOT use ObjectID where ID unpredictability is a security requirement (e.g., public-facing resource identifiers for sensitive data)
  • Do NOT use ObjectID as a "secret" identifier to restrict access

For authentication tokens and secure resource identifiers, use:

  • Cryptographically random tokens (e.g., random_bytes(32) encoded as hex or base64)
  • UUIDs generated with a CSPRNG (UUIDv4)
  • Tokens signed with bcrypt or Argon2 for password reset flows

ObjectID is fine as a primary key for MongoDB documents where the ID itself is not a security boundary - just do not conflate "unique" with "secret."


Code Examples

PHP - mongodb Extension

<?php
declare(strict_types=1);

require_once 'vendor/autoload.php';

use MongoDB\BSON\ObjectId;
use MongoDB\Client;

// Generate a new ObjectID
$id = new ObjectId();
echo $id; // e.g., 6619a3f2e4b0c1d9f8a72e30

// From an existing string
$id = new ObjectId('6619a3f2e4b0c1d9f8a72e30');

// Extract timestamp
$timestamp = $id->getTimestamp(); // int (Unix seconds)
$createdAt = new DateTimeImmutable('@' . $timestamp);
echo $createdAt->format('Y-m-d H:i:s'); // 2024-04-13 06:26:26

// Using with MongoDB client
$client = new Client('mongodb://localhost:27017');
$collection = $client->mydb->users;

// Insert a document - _id is auto-generated as ObjectId
$result = $collection->insertOne(['name' => 'Alice', 'email' => 'alice@example.com']);
$insertedId = $result->getInsertedId(); // Returns ObjectId instance

// Query by ObjectId
$user = $collection->findOne(['_id' => new ObjectId('6619a3f2e4b0c1d9f8a72e30')]);

// Find documents created after a certain date
// Construct an ObjectId from a timestamp (rest of bytes zeroed)
$afterTimestamp = (new DateTimeImmutable('2024-01-01'))->getTimestamp();
$afterId = new ObjectId(sprintf('%08x0000000000000000', $afterTimestamp));
$recentDocs = $collection->find(['_id' => ['$gt' => $afterId]]);

Python - pymongo

from bson import ObjectId
from pymongo import MongoClient
from datetime import datetime, timezone

# Generate a new ObjectID
oid = ObjectId()
print(str(oid))  # e.g., 6619a3f2e4b0c1d9f8a72e30
print(repr(oid)) # ObjectId('6619a3f2e4b0c1d9f8a72e30')

# From an existing string
oid = ObjectId("6619a3f2e4b0c1d9f8a72e30")

# Extract creation time
print(oid.generation_time)  # datetime.datetime(2024, 4, 13, 6, 26, 26, tzinfo=<UTC>)

# Using with pymongo
client = MongoClient("mongodb://localhost:27017")
db = client["mydb"]
collection = db["users"]

# Insert
result = collection.insert_one({"name": "Alice", "email": "alice@example.com"})
inserted_id = result.inserted_id  # ObjectId instance

# Query by ObjectId
user = collection.find_one({"_id": ObjectId("6619a3f2e4b0c1d9f8a72e30")})

# Range query by creation time (using ObjectId as a proxy for timestamp)
def objectid_from_datetime(dt: datetime) -> ObjectId:
    """Create an ObjectId with a specific timestamp and zeroed random/counter bytes."""
    ts = int(dt.timestamp())
    return ObjectId(f"{ts:08x}" + "0" * 16)

after = objectid_from_datetime(datetime(2024, 1, 1, tzinfo=timezone.utc))
recent_docs = collection.find({"_id": {"$gt": after}})

JavaScript - mongodb Driver

const { MongoClient, ObjectId } = require('mongodb');

// Generate a new ObjectID
const id = new ObjectId();
console.log(id.toString());       // e.g., 6619a3f2e4b0c1d9f8a72e30
console.log(id.toHexString());    // same

// From an existing string
const existing = new ObjectId('6619a3f2e4b0c1d9f8a72e30');

// Extract timestamp
console.log(existing.getTimestamp()); // Date object: 2024-04-13T06:26:26.000Z
console.log(existing.id);            // Buffer with 12 raw bytes

// Using with the driver
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();

const collection = client.db('mydb').collection('users');

// Insert
const result = await collection.insertOne({ name: 'Alice', email: 'alice@example.com' });
const insertedId = result.insertedId; // ObjectId instance

// Query by ObjectId
const user = await collection.findOne({ _id: new ObjectId('6619a3f2e4b0c1d9f8a72e30') });

// Range query: find documents created after a date
function objectIdFromDate(date) {
    const timestamp = Math.floor(date.getTime() / 1000);
    const hex = timestamp.toString(16).padStart(8, '0') + '0'.repeat(16);
    return new ObjectId(hex);
}

const afterId = objectIdFromDate(new Date('2024-01-01'));
const recentDocs = await collection.find({ _id: { $gt: afterId } }).toArray();

await client.close();

When NOT to Use ObjectID

ObjectID is a solid choice within the MongoDB ecosystem, but there are situations where it is the wrong tool:

Distributed Systems Without MongoDB Coordination

ObjectIDs are unique within a single MongoDB deployment assuming proper driver behaviour. In a distributed system where multiple independent MongoDB clusters (without coordination between them) generate IDs, ObjectIDs from different clusters can collide. If you are merging data from multiple MongoDB clusters, use UUIDs instead.

Cross-System Unique Identifiers

If your IDs need to be recognised by systems that do not speak BSON (e.g., relational databases, external APIs, event streams), ObjectID is an awkward choice. Most systems understand UUID natively. Use UUIDv4 or UUIDv7 for maximum interoperability.

When ID Exposure Is a Security Concern

As discussed in the security section: if knowing an ID reveals information you want to hide (creation time, document count, system internals), do not use ObjectID in public-facing contexts. Use cryptographically random tokens instead.

Non-MongoDB Databases

There is no benefit to using ObjectID outside MongoDB. In PostgreSQL, MySQL, or SQLite, use the UUID type. Most modern databases have native UUID support with efficient storage (16 bytes, not 24 hex characters).

When Millisecond Sort Precision Matters

If you need IDs that sort correctly even for documents created within the same second, ObjectID's second precision is insufficient. Use UUIDv7 (millisecond precision) or a Snowflake-style ID (millisecond precision with machine ID and sequence number).


When ObjectID Is Enough

MongoDB ObjectID is an elegant, compact identifier that packs creation time, machine identity, and a counter into just 12 bytes. Its embedded timestamp makes it roughly sortable without extra fields, and its client-side generation avoids database round-trips.

Understanding the structure helps you use ObjectID effectively: extract creation timestamps without extra queries, perform time-range queries using ObjectID comparisons, and avoid the common mistake of treating ObjectIDs as secret or unpredictable values.

For most MongoDB applications, ObjectID is the right default choice for _id. When you step outside MongoDB — into multi-cluster environments, cross-system IDs, or security-sensitive identifiers — reach for UUIDs or cryptographically random tokens instead.

More Articles

CSV vs JSON for Data Exchange: When Each Format Wins

A practical comparison of CSV and JSON for APIs, data pipelines, and file exports. Covers structure, parsing, streaming, schema enforcement, size, tooling, and clear guidelines for choosing the right format.

15 April, 2026

SEO for AI Search: How to Optimise for ChatGPT, Perplexity, and Google AI Overviews

How AI-powered search engines discover, evaluate, and cite web content. Practical strategies for optimising your pages for ChatGPT Browse, Perplexity, Google AI Overviews, and other AI answer engines.

14 April, 2026

Image to Base64 Data URIs: When to Inline and When Not To

A practical guide to embedding images as Base64 data URIs. Covers the data URI format, size overhead, performance trade-offs, browser caching, Content Security Policy, and clear rules for when inlining helps vs hurts.

10 April, 2026