UUIDs: Which Version to Use, and Why v4 Is Usually Wrong for a Primary Key
The versions that matter, the collision maths, and the index fragmentation nobody warns you about.
A UUID is 128 bits, written as 32 hex digits in five groups: 550e8400-e29b-41d4-a716-446655440000. The point is generating an identifier without asking anyone's permission — no central sequence, no coordination — and still not colliding with identifiers generated elsewhere.
Reading a UUID
Two positions carry meaning. The first digit of the third group is the version; the first digit of the fourth group encodes the variant and is almost always 8, 9, a or b.
550e8400-e29b-41d4-a716-446655440000
version variant
So that one is a version 4. Of the 128 bits, 6 are spent on version and variant, leaving 122 that actually vary.
The versions worth knowing
- v4 — random. 122 random bits. The default nearly everywhere.
- v7 — time-ordered random. A 48-bit Unix millisecond timestamp followed by random bits. Standardised in 2024 (RFC 9562) and the right default for anything stored in a database.
- v1 — timestamp and MAC address. Sortable, but embeds the generating machine's network address, which has leaked more information than people intended. Superseded by v7.
- v5 — namespace and name, SHA-1. Deterministic: the same namespace plus name always produces the same UUID. Useful for deriving a stable ID from something you already have.
The collision maths
122 random bits is about 5.3×1036 values. By the birthday bound, a 50% chance of one collision needs roughly 2.7×1018 UUIDs — and generating a billion a second, that is around 85 years. For a 1-in-a-billion chance of any collision you can generate about 100 trillion of them.
The practical conclusion is that v4 collisions are not a real risk provided the randomness is real. That proviso is where actual failures come from: a poorly seeded PRNG, a language's non-cryptographic random, or containers starting from identical state. Use the platform's crypto-random source, not its general-purpose one.
The problem v4 causes in a database
This is the part that bites in production. Most databases store rows in primary-key order in a B-tree. A sequential key appends to the end — one hot page, tight packing. A random key inserts at a uniformly random position, which means:
- Every insert touches a different page, so the working set becomes the whole index rather than its tail.
- Pages split repeatedly and end up roughly half empty, inflating the index and the memory needed to cache it.
- Range queries by creation time lose locality entirely.
On a large table the difference in insert throughput is not subtle. This is precisely what v7 fixes: the leading timestamp restores ordering, so inserts land at the end again while the trailing random bits keep them unguessable and uncoordinated.
If you are choosing today for a table that will grow, choose v7. If you are on v4 and the table is small, it does not matter.
Storage
The canonical text form is 36 characters. Stored as raw bytes it is 16 — less than half, and worth doing on a large table (BINARY(16) in MySQL, the native uuid type in PostgreSQL). Storing a UUID as VARCHAR(36) with a default collation is the common and costly default.
What a UUID is not
It is not a secret and it is not an authorisation. A v4 UUID is unguessable, which tempts people into using one as a capability token; the difference is that UUIDs get logged, appear in referrer headers and end up in analytics. It is also not a hash — a v4 carries no information about what it identifies, which is the point, but means it cannot be recomputed if lost.