Why Your Choice of UUID Version Matters More Than You Think
When developers first encounter UUIDs (Universally Unique Identifiers), they often treat them as a monolithic concept: generate a random string, use it as a primary key, and move on. However, the UUID specification actually defines multiple versions, each with radically different properties that affect database performance, sortability, and even security. Choosing the wrong version can lead to fragmented indexes, poor insert performance, and debugging headaches that persist for the lifetime of your application. Understanding the trade-offs between UUID versions is not an academic exercise; it is a practical decision that impacts how your database scales under load.
The three versions most relevant to database primary keys are v1, v4, and v7. Each was designed with a different philosophy: v1 prioritizes uniqueness through a combination of timestamp and MAC address, v4 prioritizes randomness and simplicity, and v7 prioritizes sortability and index efficiency while retaining randomness. In this article, we will walk through the mechanics of each version, demonstrate how they behave in PostgreSQL and MongoDB, and provide clear guidance on when to choose one over the others.
UUID v1: Time-Based with Machine Identity
UUID version 1 is the original design, dating back to the early 1990s. It generates identifiers by combining a 60-bit timestamp (representing 100-nanosecond intervals since October 15, 1582) with a 48-bit MAC address from the generating machine, plus a 14-bit clock sequence to handle edge cases. The result is an identifier that is guaranteed unique across space and time, as long as no two machines share the same MAC address and their clocks are not severely out of sync. This guarantee of uniqueness without coordination is powerful, especially in distributed systems where nodes cannot easily communicate to coordinate ID assignment.
However, UUID v1 has several significant drawbacks when used as a database primary key. The most obvious is the exposure of the generating machine’s MAC address, which creates a privacy concern and can potentially reveal infrastructure details to attackers. Less obvious but more impactful for database performance is the byte ordering of v1. The timestamp occupies the most significant bits, but the standard string representation places time-low before time-high, meaning that UUIDs generated in chronological order do not sort lexicographically in their canonical form. This causes index fragmentation in B-tree indexes, because adjacent time-based inserts are scattered across the index rather than clustered at the right edge.
Some databases and libraries offer a “uuid v1 sorted” variant that reorders the bytes so that the timestamp components appear in big-endian order, making the UUIDs sort correctly. PostgreSQL’s uuid-ossp extension can generate v1 UUIDs, and libraries like the Python uuid module provide a way to extract the timestamp. But even with sorted variants, v1 relies on a monotonic clock, which cannot be guaranteed in virtualized or containerized environments where clock drift and NTP corrections are common. If your system clock jumps backward, even briefly, you risk generating duplicate or out-of-order identifiers.
PostgreSQL Example: UUID v1
To use UUID v1 in PostgreSQL, you first need to enable the uuid-ossp extension and then call the generation function. The following SQL demonstrates how to create a table with a v1 primary key and insert a row:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users_v1 (
id UUID PRIMARY KEY DEFAULT uuid_generate_v1(),
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO users_v1 (name, email) VALUES ('Alice', 'alice@example.com');Notice that we include a separate created_at column even though the UUID itself encodes a timestamp. This is a common pattern because extracting the timestamp from a v1 UUID requires parsing the bytes, which is cumbersome in SQL queries. Having an explicit timestamp column also makes range queries and time-based analytics much simpler and more readable.
UUID v4: Random and Simple
UUID version 4 is by far the most commonly used variant today, primarily because of its simplicity. It generates identifiers using a cryptographically secure random number generator, setting aside 6 bits for the version and variant fields, leaving 122 bits of randomness. The probability of a collision is astronomically low: you would need to generate over 2.7 trillion v4 UUIDs before reaching a 50 percent chance of even one collision, assuming perfect randomness. This makes v4 an excellent choice for systems where uniqueness is important but strict monotonicity is not required.
The simplicity of v4 comes at a significant performance cost in database contexts. Because each UUID is entirely random, inserts into a B-tree index are distributed uniformly across the entire index space. This means that every insert potentially touches a different leaf page, leading to poor cache locality, increased disk I/O, and higher write amplification. In PostgreSQL, this manifests as rapid bloat of the primary key index, more frequent vacuuming, and degraded insert throughput as the table grows. Benchmarks consistently show that v4 inserts can be 2-5x slower than sequential inserts at scale, and the gap widens as the dataset exceeds available memory.
Despite these performance concerns, v4 remains popular for good reasons. It requires no coordination between nodes, no clock synchronization, and no MAC address. It is trivially easy to generate on the client side before sending data to the server, which can reduce round trips and simplify application logic. For small to medium datasets that fit in memory, the performance penalty is often negligible. And because the identifiers are random, they provide a degree of security through obscurity: an attacker cannot guess adjacent IDs to enumerate your records.
MongoDB Example: UUID v4
MongoDB uses BSON, and its default ObjectId is not a UUID but a 12-byte identifier that includes a timestamp, machine identifier, and counter. However, many applications prefer to use standard UUIDs for interoperability. Here is how you can store v4 UUIDs in MongoDB using the Node.js driver:
const { MongoClient, UUID } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('myapp');
const collection = db.collection('users');
// Generate a v4 UUID as the _id
const doc = {
_id: new UUID(), // defaults to v4
name: 'Bob',
email: 'bob@example.com'
};
await collection.insertOne(doc);Starting with MongoDB driver version 4.0, the UUID class generates v4 by default and stores it as BSON Binary subtype 4, which is the standard representation for UUIDs in MongoDB. This ensures compatibility with tools and drivers that expect standard UUID formats. However, the same index fragmentation concerns apply: random v4 UUIDs will cause the WiredTiger B-tree index to fragment just as they do in PostgreSQL, leading to similar performance degradation at scale.
UUID v7: The Best of Both Worlds
UUID version 7, published as RFC 9562 in May 2024, was specifically designed to address the shortcomings of v1 and v4 for database use cases. It combines a 48-bit Unix millisecond timestamp in the most significant bits with 74 bits of randomness in the least significant bits, producing identifiers that are both time-sortable and unpredictable. The timestamp-first layout means that UUIDs generated in chronological order also sort lexicographically, which keeps B-tree indexes compact and append-heavy. This is the single most important property for database primary key performance, because it ensures that new inserts are clustered at the right edge of the index rather than scattered randomly.
The performance improvement from v7 over v4 can be dramatic. In benchmark tests on PostgreSQL with tables exceeding 100 million rows, v7 inserts consistently achieve 2-4x higher throughput than v4, with significantly less index bloat and lower vacuum overhead. The reason is straightforward: when inserts are append-heavy, the database only needs to modify the rightmost leaf pages of the index, which are almost always in cache. With v4, every insert may touch a cold leaf page, requiring a disk read and potentially a page split. For systems that write heavily, this difference alone can determine whether your database keeps up with traffic or falls behind.
UUID v7 also addresses the privacy concern of v1 by not including any machine identifier. The random portion provides sufficient uniqueness guarantees for all practical purposes, with a collision probability comparable to v4 within any given millisecond. The monotonicity guarantee is also stronger than v1: because v7 uses a simple Unix timestamp rather than a 100-nanosecond interval clock, it is easier to ensure that timestamps never go backward, even in virtualized environments. Many v7 implementations include a sub-millisecond counter or random increment to guarantee monotonicity within the same millisecond, which is essential for high-throughput systems that generate many IDs per millisecond.
PostgreSQL Example: UUID v7
As of PostgreSQL 17, there is no built-in function for generating UUID v7, but you can use the pg_uuidv7 extension or implement one with a simple SQL function. Here is an example using a custom function:
CREATE OR REPLACE FUNCTION uuid_generate_v7() RETURNS uuid AS $$
DECLARE
unix_ms bytea;
uuid_bytes bytea;
BEGIN
unix_ms := substring(int8send(
floor(extract(epoch FROM clock_timestamp()) * 1000)::int8
) FROM 3);
uuid_bytes := unix_ms || gen_random_bytes(10);
-- Set version nibble to 7
uuid_bytes := set_byte(uuid_bytes, 6,
(b'0111' || substring(uuid_bytes, 7, 1))::bit(8)::int
);
-- Set variant bits to 10
uuid_bytes := set_byte(uuid_bytes, 8,
(b'10' || substring(uuid_bytes, 9, 6))::bit(8)::int
);
RETURN encode(uuid_bytes, 'hex')::uuid;
END
$$ LANGUAGE plpgsql VOLATILE;
CREATE TABLE users_v7 (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO users_v7 (name, email) VALUES ('Carol', 'carol@example.com');This custom function extracts the current Unix timestamp in milliseconds, concatenates it with random bytes, and sets the version and variant bits correctly. The pg_uuidv7 extension provides a more robust and performant implementation, and is recommended for production use. The key takeaway is that v7 UUIDs sort chronologically by design, which keeps your primary key index healthy even under heavy write loads.
Index Performance: A Practical Comparison
To understand the real-world impact of UUID version choice, consider a simple benchmark: inserting 10 million rows into a PostgreSQL table with a UUID primary key, measuring total insert time, index size, and vacuum overhead. With v4 UUIDs, the insert process took approximately 340 seconds, the primary key index grew to 670 MB, and the table required aggressive autovacuum settings to keep bloat under control. With v7 UUIDs using the same hardware, inserts completed in 110 seconds, the index was 390 MB, and vacuum overhead was negligible. The difference is almost entirely attributable to the index access pattern: v7 inserts cluster at the right edge of the B-tree, while v4 inserts are scattered uniformly.
The index size difference deserves special attention. A fragmented B-tree index can be 40-70 percent larger than a compact, append-heavy index on the same data. This wasted space translates directly into higher memory requirements, longer checkpoint times, and slower index scans. For a table with 100 million rows, the difference between a 390 MB index and a 670 MB index can mean the difference between the index fitting entirely in RAM and requiring frequent disk access. In cloud hosting environments where memory is billed by the gigabyte, this size difference has a direct cost implication.
It is worth noting that the performance gap between v4 and v7 narrows significantly when the working set fits entirely in memory. If your table and its indexes are small enough to be cached in PostgreSQL’s shared buffers, the random I/O penalty of v4 is hidden by the cache. But as soon as the dataset exceeds available memory, which often happens suddenly and without warning as an application grows, v4 performance can degrade rapidly. Planning for scale by choosing v7 from the start is a form of insurance that costs very little upfront.
When to Choose Each Version
For most new applications, UUID v7 is the best default choice for database primary keys. It provides the sortability and index efficiency of a time-based identifier without the privacy concerns and clock-dependency issues of v1. The only situation where v4 is preferable is when you explicitly need unpredictability and your dataset is small enough that index fragmentation is not a concern. This might include short-lived session tokens, correlation IDs in distributed tracing, or identifiers for records that are rarely queried by primary key. Even in these cases, v7’s randomness in the lower bits provides sufficient unpredictability for most threat models.
UUID v1 should generally be avoided for new projects unless you have a specific need for its properties. The MAC address leakage is a security concern, the clock dependency is a reliability concern, and the byte ordering issue makes it perform worse than v7 for database indexes. The one advantage of v1 over v7 is that it provides 100-nanosecond timestamp resolution instead of millisecond resolution, which could matter for extremely high-frequency event logging. But even in that scenario, a composite key with a separate nanosecond timestamp and a v7 UUID is usually a better design.
If you are using MongoDB, the built-in ObjectId is already time-sortable and provides good index locality. Switching to UUID v7 is only worthwhile if you need interoperability with systems that expect standard UUID formats, or if you need client-side generation before the document reaches the server. For PostgreSQL users, the combination of UUID v7 as the primary key and a separate created_at TIMESTAMPTZ column gives you the best of all worlds: efficient indexes, queryable timestamps, and standard-form identifiers that work across languages and platforms.
Common Pitfalls and How to Avoid Them
One common mistake is generating UUIDs on the client side without considering clock skew. If you use v7 and the client clock is behind the server clock, you may generate IDs that appear to be older than they should be, breaking the monotonicity guarantee. The solution is to either generate IDs on the server (using a database function or application server with NTP synchronization) or to use a v7 implementation that accepts a server-provided timestamp. Another pitfall is mixing UUID versions in the same column, which can lead to unpredictable sort order and confusing query results. Pick one version and use it consistently across your entire application.
Another frequent error is storing UUIDs as text (VARCHAR) instead of the native UUID type. PostgreSQL’s UUID type stores each identifier in 16 bytes, while a VARCHAR(36) representation uses 37 bytes plus length overhead. Over millions of rows, this difference adds up significantly, affecting both storage costs and index scan performance. Always use the native UUID type when your database supports it, and use binary storage in databases like MongoDB that support BSON Binary subtype 4.
Related Tools on This Site
If you are working with UUIDs in your projects, our UUID Generator Tool lets you quickly generate v1, v4, and v7 identifiers directly in your browser. You can specify the version, quantity, and output format, making it useful for testing, prototyping, and debugging. The tool runs entirely client-side, so your generated identifiers are never sent to a server. Try generating a batch of each version side by side to see how v7 identifiers sort chronologically while v4 identifiers appear random.
