UUID v4 vs v7: Choosing a Primary Key
Published 2026-10-31
Both are 128-bit identifiers with negligible collision risk. Both are copy-paste safe. The choice comes down to one property: is the value sortable by creation time?
That property decides how your database indexes behave at scale.
What is actually inside
| v4 | v7 | |
|---|---|---|
| Layout | 122 random bits | 48-bit Unix ms timestamp + 74 random bits |
| Sortable by time | No | Yes |
| Guessable | No | Partially — leaks creation time to the millisecond |
| Standardised | RFC 4122 | RFC 9562 (2024) |
| Index behaviour | Scattered inserts | Appends |
v4 is pure randomness. v7 puts the timestamp first, then randomness, which means lexicographic order equals creation order.
Why the layout matters: index locality
A B-tree index keeps keys in sorted order. When you insert a new row:
- Sequential key (auto-increment, v7) → the new key goes at the end, which is almost always in the cache. One page touched.
- Random key (v4) → the new key lands in a random page, which is likely not in memory, so the engine must read it from disk, modify it, and write it back.
That random write is not a theoretical concern. On a table with a few hundred million rows and a working set larger than RAM, v4 primary keys reduce insert throughput markedly and inflate the index (pages split more often, fill factor drops).
This is the same argument that makes UUIDv7 and ULID attractive: they are "UUID-shaped" but behave like an auto-increment for inserts.
When v4 is the right answer
Randomness is a feature when the identifier is exposed to users:
- Public URLs and share links. A v7 reveals when the resource was created, to the millisecond. For a support ticket, that is a minor leak; for a "shared document" link, it is metadata you would rather not publish.
- Guessing resistance. v7 is not guessable in practice (74 random bits follow the timestamp), but v4 is trivially unguessable.
- Anything that must not correlate with creation time — for example, an ID used to bucket users, where a time-derived prefix leaks the growth curve.
A common compromise: v7 as the internal primary key (good index behaviour), plus a v4 public token in a separate column for external references.
What v7 leaks
The first 48 bits are a millisecond timestamp. Given one ID you can recover the creation time exactly. That is usually acceptable, and sometimes useful — but it must be a decision, not an accident.
If the leak matters and you still want sortable keys, ULID (also time-prefixed, Crockford base32) has the same property, so switching does not help. The alternative is a random key plus a separate created_at column and a covering index — which reintroduces the scattered insert.
Generating them
crypto.randomUUID() // v4, available in Node 19+ and all modern browsers
For v7, most runtimes do not ship it yet; use a small library or construct it: 48-bit timestamp, version nibble, 12 random bits, variant, 62 random bits.
Two rules regardless of version:
- Use a cryptographic source.
Math.random()is not one — collisions and predictability both follow from that. - Never derive a UUID from user data (email, name) with a hash. That identifies users across systems and cannot be rotated.
UUID Generator produces v4 and v7 in bulk, with options for uppercase, no hyphens, and random tokens — handy when you need a column of test IDs or a batch of API keys.
The decision table
| Situation | Use |
|---|---|
| High-volume table, key never exposed | v7 (or auto-increment) |
| Key appears in user-facing URLs | v4 |
| Existing schema with v4 | Keep it; do not migrate for this alone |
| Need to sort by creation without a timestamp column | v7 |
| Distributed writers, no central sequence | v7 |
The migration advice matters: changing a primary key type on a large table is expensive and risky. If v4 is already working, the insert-locality argument is usually not worth a rewrite — apply it to new tables instead.
Summary
v7 buys insert locality at the cost of leaking creation time. v4 buys unpredictability at the cost of random writes. Neither is "better"; they encode different priorities. Pick based on whether the key is public and whether the table will be large.