UUID v4 vs v7: which one to use, and what v7 gives away

Updated 5 min read

A table with a random UUID primary key is fast at 10,000 rows and noticeably slower once the index no longer fits in memory. That is the usual reason people start asking about UUID v7, and the answer is mostly about where new rows land in the index.

Why random keys hurt a B-tree

A v4 UUID has 122 random bits, so a new key can sort anywhere in the index. Each insert touches an arbitrary leaf page, which must be in memory or read from disk, and a full page splits to make room. Sequential keys behave differently: every insert goes to the right edge of the index, the same few pages stay hot, and pages fill completely before the next one starts.

A v7 UUID starts with a millisecond timestamp, so ids created later sort after ids created earlier. Inserts cluster at the right edge the way an auto-increment column does, while the id can still be generated by any client without asking the database. If your table is small, or the UUID is never an index you range over, the difference will not matter and v4 is fine.

What is inside a v7

RFC 9562 (May 2024, which supersedes RFC 4122) defines the layout as 128 bits:

BitsFieldValue
48unix_ts_msmilliseconds since the Unix epoch, big-endian
4ver0111, the digit 7
12rand_arandom (or a counter)
2var10
62rand_brandom

That leaves 74 random bits, against 122 in v4. In text form the version digit is the first character of the third group and the variant is the first character of the fourth: xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx, where y is 8, 9, a or b. Because the timestamp is the leading 12 hex digits, you can read it back:

const ms = parseInt(id.replace(/-/g, '').slice(0, 12), 16);
new Date(ms).toISOString(); // when this id was created

Forty-eight bits of milliseconds last until the year 10889, so overflow is not a concern.

Ordering inside one millisecond

Two ids created in the same millisecond share a timestamp, and the next 74 bits decide their order. If they are freshly random, the second id can sort before the first. RFC 9562 describes ways to avoid this, such as using part of rand_a as a counter, or incrementing the random field when the clock has not moved. The UUID generator linked below takes the second approach: ids made in the same millisecond keep incrementing the random field, so a list of 1000 comes out strictly ascending. It also never lets its timestamp go backwards if the system clock does.

Two limits remain whatever the generator does. Ordering across several machines is only as good as their clocks, so v7 gives roughly-by-time, not a total order of events. And a sorted text column only sorts correctly if every value is spelled the same way: mixing uppercase and lowercase hex, or hyphenated and unhyphenated forms, breaks a plain string comparison.

What a v7 gives away

The first 48 bits are a creation time anyone can decode, as the snippet above shows. For a database key that is mostly harmless, but it matters in a few places:

  • A public URL such as /orders/<uuid> tells a visitor when that order was placed, and the spread of timestamps across ids reveals your order volume.
  • An id used as an unguessable token or reset link should come from a purpose-built random source with at least 128 random bits, not from a UUID of either version. v4 from a CSPRNG is hard to guess, but nothing about the format promises that.
  • v1, the old time-based version, is worse: it stores a 60-bit timestamp, a clock sequence and a node id that was often the machine’s MAC address. Its timestamp is also stored low bits first, so v1 ids do not sort by time as text.

Storage matters more than the version

A UUID is 16 bytes, yet it is commonly stored as a 36-character string, which is 36 bytes before the overhead. Use the native type where there is one: PostgreSQL has uuid, and MySQL’s usual answer is BINARY(16). Every secondary index copies the primary key, so a fat key is paid for several times. Storing v7 as text still sorts correctly, but it costs more space; and a binary column sorts bytewise, which is exactly timestamp order for v7.

Choosing

SituationUse
Primary key in a table that will grow largev7
Identifier that must not reveal when it was madev4
Test fixtures, one-off ids, small tableseither; v4 is the default almost everywhere
Placeholder or “no value” markerNIL, 00000000-0000-0000-0000-000000000000
Unguessable secretneither; use a dedicated random token

Two other versions appear in RFC 9562 and are rarely the right pick. v6 reorders v1’s timestamp so it sorts, which only makes sense for systems already on v1, and v8 is left for custom layouts. The same RFC also adds a Max UUID of all ones as the counterpart to NIL.

Do random UUIDs collide?

With 122 random bits, the birthday bound puts a 50% chance of a duplicate at roughly 2^61 ids, about 2.3 x 10^18. A real table never gets near that. Duplicates that do occur in practice come from a bad source: a hand-rolled “UUID function” built on Math.random(), or a cloned virtual machine image that restarts a generator from the same seed. crypto.randomUUID() or your platform’s UUID library avoids both. For v7 only ids from the same millisecond can collide, and within one process the counter approach above rules that out.

Generating them

In a browser, crypto.randomUUID() returns a v4 and needs a secure context (https or localhost). There is no built-in v7 in the browser, which is the gap the generator tool fills: it offers v4, v7 and NIL, 1 to 1000 at a time, with toggles for uppercase, hyphens and the urn:uuid: form. Entropy comes from crypto.getRandomValues, and generation happens in the page, so no ids are sent anywhere.

Open the tool: UUID Generator

Back to guides

More guides