Understanding UUID v7: Why Time-Sortable Identifiers Change Everything
A deep dive into UUID v7 (RFC 9562), why it's a huge upgrade from v4 for databases, and how to use it in production.
For decades, UUIDs were random 128-bit values. Universally unique, sure — but also a nightmare for databases. Insert a million v4 UUIDs into a B-tree index and watch your index fragmentation go through the roof, because every insert lands at a random leaf node, causing constant page splits.
UUID v7 fixes this. It's still a 128-bit UUID, still globally unique in practice, but it embeds a Unix millisecond timestamp in the high bits — which means new UUIDs sort after old ones in any lexicographic comparison.
The anatomy of UUID v7
A UUID v7 has this structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b (continued) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The first 48 bits are the Unix timestamp in milliseconds. Then 4 bits for the version (7), 12 bits of random data, 2 bits for the variant, and finally 62 bits of random data. That's 74 bits of randomness — enough that collisions are still astronomically unlikely.
Why this matters for databases
When you insert a v7 UUID into a B-tree index, the new entry almost always lands at the end of the index — because the timestamp is monotonically increasing. No random page splits. No fragmentation. Cache-friendly sequential writes.
For PostgreSQL users with uuid primary keys, switching from v4 to v7 typically gives 2-4x insert throughput on large tables. The win is even bigger when you have multiple secondary indexes that all share the same primary key ordering.
When v7 isn't the right choice
UUID v7 has one downside: it leaks the creation timestamp. If you're building a system where timestamp privacy matters (say, anonymized user IDs that shouldn't be linkable to creation time), v4 or even v8 with custom entropy is better.
It also doesn't help with sharding decisions. If you need geographically distributed IDs, you typically want something like ULID with a custom epoch, or Snowflake IDs that encode datacenter/worker info.
Generating v7 in the browser
Modern browsers don't have native v7 support yet, but generating one is straightforward:
function uuidV7() {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
const now = Date.now();
// First 6 bytes = unix timestamp in ms (big-endian)
const high = Math.floor(now / 2 ** 16);
const low = now & 0xffff;
bytes[0] = (high >> 24) & 0xff;
bytes[1] = (high >> 16) & 0xff;
bytes[2] = (high >> 8) & 0xff;
bytes[3] = high & 0xff;
bytes[4] = (low >> 8) & 0xff;
bytes[5] = low & 0xff;
// Set version 7
bytes[6] = (bytes[6] & 0x0f) | 0x70;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
// Format as standard UUID string
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));
return `${hex.slice(0,4).join('')}-${hex.slice(4,6).join('')}-${hex.slice(6,8).join('')}-${hex.slice(8,10).join('')}-${hex.slice(10,16).join('')}`;
}
TextOpal's UUID Generator runs this exact code locally — your timestamps never leave the browser. Generate a thousand at a time if you need test data.
What's next
If you're already using v4 UUIDs in production, you don't have to migrate. New tables can use v7 from day one, and you'll see the benefits immediately. The format is fully RFC-compliant and parses cleanly in every major database.
The shift to time-sortable identifiers is one of those small architectural decisions that pays dividends for years. Try v7 in your next project.