UUIDs show up everywhere: database primary keys, distributed system message IDs, API idempotency keys, session tokens. For years, v4 was the default — call crypto.randomUUID() and move on. Then RFC 9562 (published May 2024) formally standardized UUID v7, and the conversation shifted. v7 gives you time-ordered IDs with the same uniqueness guarantees. The tradeoff isn’t obvious until you understand what happens inside a B-tree index.
How UUID v4 works
A v4 UUID is 128 bits of randomness with 6 bits reserved for version and variant markers. The version nibble (bits 48–51) is set to 0100, and the variant bits (bits 64–65) are set to 10. That leaves 122 bits of entropy — enough to generate a billion UUIDs per second for 86 years before hitting a 50% collision probability.
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
^ ^
version variant (8, 9, a, or b)Generation is simple: fill 128 bits from a cryptographic random source, stamp the version and variant bits, format as hex with dashes. No coordination, no state, no clock. That simplicity is v4’s strength — and its weakness.
How UUID v7 works
A v7 UUID encodes a 48-bit Unix timestamp in milliseconds in the most significant bits, followed by 4 version bits, 12 bits of sub-millisecond randomness or sequence, 2 variant bits, and 62 bits of random data.
tttttttt-tttt-7sss-yxxx-xxxxxxxxxxxx
|____________| | | |
48-bit ms ver sub 62 random bits
timestamp 7 msThe timestamp occupies the high-order bits, so v7 UUIDs sort chronologically when compared as raw bytes or strings. Two UUIDs generated one second apart will always sort in the order they were created. Two generated in the same millisecond are still unique (the random suffix handles that) but their relative order within that millisecond isn’t guaranteed unless the implementation uses a monotonic counter in the sub-millisecond bits.
The 48-bit millisecond timestamp won’t overflow until the year 10889. You don’t need to worry about that.
The database index problem
This is where the choice between v4 and v7 actually matters. Most databases store primary key indexes in a B-tree (or a variant like B+tree). B-trees keep data sorted and split leaf pages when they fill up. The insertion pattern determines how efficiently the tree grows.
v4: random insertion
Because v4 UUIDs are random, each new insert lands on a random leaf page. Once the table is large enough that the index doesn’t fit in memory, every insert triggers a random disk read to find the right page, modify it, and write it back. This is called page splitting under random load, and it creates several problems:
- Write amplification.The database reads a page, inserts the row, and often splits the page in half because the new key falls in the middle. Over time, pages average around 50–70% full instead of close to 100%.
- Cache thrashing. The working set for the index is the entire index, not just recent pages. The buffer pool cycles through pages rapidly, evicting useful data.
- Fragmentation. Leaf pages are scattered across disk. Range scans (which rarely happen on UUID columns, but sometimes do for time-based queries joined through them) touch many non-adjacent pages.
At small scale, none of this matters. A table with a few million rows and an index that fits in RAM performs fine with v4 keys. The problems start when the index outgrows memory — typically in the tens of millions of rows, depending on row size and available RAM.
v7: append-mostly insertion
Because v7 UUIDs are time-ordered, new inserts always land at or near the right edge of the B-tree. This is the same insertion pattern as an auto-incrementing integer. Pages fill sequentially, splits are rare and happen at the end, and the buffer pool only needs to keep recent pages hot. Page utilization stays above 90%.
Benchmarks vary by database, but the general shape is consistent: PostgreSQL with v4 UUID primary keys on a 100M-row table sees 2–5x slower inserts than the same table with v7 UUIDs, once the index exceeds available shared_buffers. MySQL/InnoDB (where the primary key is the clustered index) shows even larger gaps because random primary keys fragment the actual table data, not just the index.
When v4 is still fine
v7’s benefits are specific to ordered storage. Plenty of use cases don’t care about order:
- Idempotency keys. You generate one per request, look it up by exact match, and delete it after a TTL. Order is irrelevant.
- Tokens and nonces.Session tokens, CSRF tokens, password reset tokens. You want unpredictability, not sortability. v4’s full randomness is actually a feature here — a v7 UUID leaks its creation timestamp, which might be information you don’t want to expose.
- Small tables. If your table will never exceed a few million rows, the index fits in memory regardless. Use whatever is convenient.
- Hash-partitioned systems.If you’re sharding by hash of the primary key, time-ordered keys don’t help. They can even hurt — recent inserts all hash to nearby values, creating hot partitions if the hash function doesn’t redistribute well.
- External IDs you don’t control.If a third-party API gives you v4 UUIDs, store them as-is. Don’t convert.
The timestamp leakage question
v7 UUIDs encode their creation time to millisecond precision. Anyone who sees the UUID can extract the timestamp:
// Extract timestamp from a v7 UUID
const uuid = "01904f65-5a80-7f3a-9c4b-2d8e6f1a3b5c";
const hex = uuid.replace(/-/g, "").slice(0, 12);
const ms = parseInt(hex, 16);
console.log(new Date(ms));
// 2024-06-15T12:34:56.000Z (approximate)For most applications this is harmless — the creation time of a database row isn’t sensitive. But if you’re generating IDs for user-facing entities where the creation time shouldn’t be guessable (e.g., anonymous feedback submissions, whistleblower reports), use v4.
Generating v7 in practice
Browser JavaScript doesn’t have a built-in v7 generator yet. crypto.randomUUID() returns v4. To generate v7, you need to construct the bytes manually:
function uuidv7(): string {
const now = Date.now();
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// 48-bit timestamp in ms (big-endian)
bytes[0] = (now / 2**40) & 0xff;
bytes[1] = (now / 2**32) & 0xff;
bytes[2] = (now / 2**24) & 0xff;
bytes[3] = (now / 2**16) & 0xff;
bytes[4] = (now / 2**8) & 0xff;
bytes[5] = now & 0xff;
// Version 7
bytes[6] = (bytes[6] & 0x0f) | 0x70;
// Variant 10
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes]
.map(b => b.toString(16).padStart(2, "0"))
.join("");
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20),
].join("-");
}In server-side environments, most languages have libraries. PostgreSQL 13+ can generate v4 with gen_random_uuid(); for v7, use the pg_uuidv7 extension. Python has uuid7 on PyPI. Go has github.com/google/uuid which added v7 support. Java has java.util.UUID with v7 factory methods since JDK 21 (via Generators.timeOrderedEpoch() from uuid-creator for earlier versions).
Migration considerations
If you have an existing table with v4 primary keys, switching to v7 for new rows is safe — both versions are valid UUIDs stored in the same column type. Old rows keep their v4 IDs; new rows get v7. The index won’t become perfectly sequential overnight, but new inserts will land in order, which is the part that matters for write performance.
Don’t rewrite existing primary keys. The foreign key cascade alone makes it not worth the effort, and the old random keys are already in the tree — moving them won’t reclaim the fragmented space without a full index rebuild (REINDEX in PostgreSQL, OPTIMIZE TABLE in MySQL).
The short answer
Use v7 as your default for database primary keys and any ID that benefits from chronological ordering. Use v4 when you need unpredictability, when the ID isn’t stored in an ordered index, or when you’re working with a system that already uses v4. Don’t overthink it — both versions produce unique, collision-resistant identifiers. The difference is how your database feels about them at scale.
Need to generate some UUIDs quickly? The UUID Generator supports both v4 and v7 with bulk generation up to 1,000 at a time.