DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Why v7 UUIDs beat v4 for database keys (and how to hand-roll both)

I build one small browser tool a day and write down what I learned. Day 25 was a UUID generator. What started as "make some random IDs" turned into a proper look at how the bits are laid out, and why the newer v7 format is quietly the better default for a primary key.

Live tool: https://dev48v.infy.uk/solve/day25-uuid.html

A UUID is just 16 bytes with a few fixed bits

A UUID is a 128-bit number, written as 32 hex digits grouped 8-4-4-4-12. That is about 3.4x10^38 possible values, which is the whole point: any machine can pick one and trust it will not clash with any other UUID minted anywhere, ever. It carries no meaning — it is an identifier, not data.

The reason UUIDs exist at all is coordination. The classic database ID is 1, 2, 3... from a central counter, and that works great until you have more than one writer. Two servers, an offline mobile app, or a sharded database cannot all ask one counter for the next number without a round-trip and a lock. UUIDs sidestep that entirely: each node generates its own IDs locally, with zero coordination, and they still do not collide. A client can even create the ID before the row ever reaches the server.

Version 4: 122 random bits

v4 is the one most people mean by "UUID". Fill all 16 bytes with cryptographic randomness, then overwrite two small fields so tools can recognise the format:

const b = new Uint8Array(16);
crypto.getRandomValues(b);        // never Math.random()
b[6] = (b[6] & 0x0f) | 0x40;      // version 4
b[8] = (b[8] & 0x3f) | 0x80;      // variant 10xx
Enter fullscreen mode Exit fullscreen mode

Two things get pinned. The high nibble of byte 6 becomes 4 — that is the digit right after the second hyphen, and it is how any parser knows the scheme. The top two bits of byte 8 become 10, which is why the 17th hex digit of almost every UUID you see is 8, 9, a or b. Everything else stays random: 122 bits of it.

Is "random and never collides" a contradiction? The birthday paradox says collisions become likely around the square root of the space, which for 122 bits is roughly 2.7x10^18 UUIDs. Concretely: you would have to generate a billion UUIDs per second for about 85 years to hit a 50% chance of a single duplicate. Treating them as unique is safe.

If all you need is a random ID, the browser already ships one: crypto.randomUUID() returns a compliant v4. I hand-rolled it anyway to see where the bits go — and because no built-in makes v7 yet.

Version 7: put the time on the front

v7 (standardised in RFC 9562, 2024) is the modern sweet spot. Its first 48 bits are simply the Unix time in milliseconds, big-endian; the rest is random. Because the timestamp sits at the most-significant end, a UUID made later is numerically larger, so v7s sort chronologically as plain strings — no separate created_at column needed.

let t = Date.now();
const b = new Uint8Array(16);
crypto.getRandomValues(b);
for (let i = 5; i >= 0; i--){ b[i] = t % 256; t = Math.floor(t / 256); }
b[6] = (b[6] & 0x0f) | 0x70;      // version 7
b[8] = (b[8] & 0x3f) | 0x80;      // variant 10xx
Enter fullscreen mode Exit fullscreen mode

One gotcha: JavaScript bitwise operators only work on 32 bits, but the timestamp is 48. So I peel the bytes off with % 256 and Math.floor(... / 256) instead of shifts, and the full value survives. Reading it back is the reverse — the first 12 hex digits parsed as base 16, which parseInt handles fine since 48 bits is well under the safe-integer limit.

The part that actually matters: index locality

Here is why v7 is the better key. A v4 primary key is random, so consecutive inserts land in random positions of the database's B-tree index. That scatters writes across the whole index, triggers constant page splits, and thrashes the cache — a real, measurable slowdown on high-insert tables.

A v7 key is time-ordered, so every insert appends near the end of the index, exactly like an auto-increment integer. Pages fill sequentially and stay hot. You keep every distributed-generation benefit of a UUID and regain the index behaviour of a serial ID. That is why Postgres, MySQL and several ORMs now recommend v7 for new primary keys.

For contrast, the old v1 also embedded time — but it built the ID from the machine's MAC address, which leaks which device generated it and when (it helped trace the author of the Melissa virus). v7 keeps the time-ordering and drops the MAC in favour of randomness.

Try it

The tool generates v4 and v7 live, does bulk runs of 1/5/10/100, toggles formats (hyphens, uppercase, braces, urn:uuid:), and has an inspector: paste any UUID and it validates the shape, shows the version and variant, and for a v7 extracts the embedded timestamp as a real date. There is also a little demo that makes six v7s a few milliseconds apart, shuffles them, then sorts the strings — and they snap back into creation order.

https://dev48v.infy.uk/solve/day25-uuid.html

Takeaway: a UUID is 16 bytes with a couple of fixed bits, and picking v7 over v4 for a key is a free performance win.

Top comments (0)