Data

What a UUID is, and which version you actually want

128 bits, five fields, 32 hex digits, and a version nibble that decides how much of it is random. Here is the collision arithmetic and how to pick a version.

A UUID is a 36-character string: 32 hexadecimal digits in five groups, separated by hyphens, in the widths 8-4-4-4-12. The widths look arbitrary. They are not. Each group is a named field with a job, and once you can read the fields you can pick up any identifier you meet and say how it was built, how much of it is genuinely random, and whether sorting it means anything.

Here is a real one, the version 4 from the RFC's own example set: 9c5b94b1-35ad-49bb-b118-8e8fc24abf80. Two characters in that string decide almost everything about it, and most people who work with these every day have never looked at either. The first character of the third group is a 4. The first character of the fourth group is a b. Those two nibbles are the version and the variant, and they are the difference between an identifier and a liability.

The digits are named fields

The register is 128 bits wide, which is why there are 32 hex digits — four bits to a digit. The five groups are not a formatting accident; they are fields, and in the original design each one carried a specific piece of information.

Field Hex digits Bits Random bits in a v4
time_low83232
time_mid41616
time_hi_and_version41612
clock_seq41614
node124848
Total32128122

The last column is this whole article compressed into a column. A version 4 identifier carries 122 bits of randomness, not 128, and the six missing bits are exactly the ones the table shows being spent on structure.

The field names come from the first design, where the value was meant to be globally unique by combining a timestamp with the network card's hardware address. That plan is long dead — Ethernet addresses stopped being unique the moment virtual machines arrived, and publishing a MAC address turned out to be a privacy problem — but the names stuck, and the versions that came later reuse the same slots for different jobs.

Why a v4 is 122 bits and not 128

Set out the budget. You start with 128 bits. The version field is four bits wide and it holds the number four, so every version 4 in existence has identical bits there and they carry no information. The variant field is two bits, and across the whole RFC family it reads 10. Also fixed. Four plus two is six, and 128 minus 6 leaves 122 bits for an implementation to fill from its random source.

You can watch the accounting happen in the example. The third group is 49bb: the leading 4 is the version, and only the remaining twelve bits of that group are random. The fourth group is b118: the leading b is 1011 in binary, the first two of those bits are the variant marker 10, and the other fourteen bits of the group are random. Add the columns: 32 plus 16 plus 12 plus 14 plus 48.

Why the difference matters shows up in the next section, because collision arithmetic runs on the square root of the space. Six bits off the exponent is a factor of eight in the answer, which is not a rounding error.

The birthday problem, worked

Collisions are not about how many values you have looked at. They are about how many pairs of values could have matched. With N possibilities and k of them drawn, the number of pairs is about k squared over two, so the chance of at least one accidental match is roughly:

P(collision) ~ 1 - e^(-k^2 / 2N)

k ~ 1.1774 x sqrt(N)       for a 50% chance

That constant is the square root of twice the natural logarithm of two, and it is the same constant in every birthday problem you will ever do. Now put the real numbers in. A version 4 has 122 random bits, so N is 2 to the 122. The square root of that is 2 to the 61, or 2,305,843,009,213,693,952. Multiply by 1.1774 and you land at about 2.7 x 10^18.

Two and a half quintillion identifiers before the odds of a single collision anywhere in the set reach even money. Put a rate on it. Generating a billion of them every second, around the clock, you need roughly 86 years to get there. A system minting a hundred thousand a second takes about 860,000 years. And the scale real systems reach is nowhere close: a trillion stored version 4s — far more than a busy application accumulates in its lifetime — carries a collision chance of about one in ten trillion. This is why random keys are a solved problem in a database, and why the six structural bits do not cost you anything there.

For a security token, the same arithmetic answers the wrong question. An attacker is not waiting for any two values to match; they are trying to hit one specific value, and each guess is one shot at 1 in 2 to the 122. Brute force is not the threat and never was. The threat is that the number in the exponent is not 122 in your implementation. Shorten an identifier by keeping the first eight hex digits — a thing people do to make a tidier key — and you have kept 32 bits, not "part of 122". The even-money point for 32 bits is 1.1774 times 2 to the 16, which is about 77,000 values. A table does not have to be large to hold 77,000 rows. Eight characters of a UUID is not a short UUID; it is a different identifier wearing the same clothes.

Time-ordered ids and the page split

Version 1 is the original: a 60-bit timestamp counting 100-nanosecond intervals from 1582, a 14-bit clock sequence, and a 48-bit node field that was meant to hold the machine's hardware address. Version 6 is the same data with the timestamp parts reordered so the whole value sorts chronologically. Version 7 drops the old epoch and the node field altogether and takes 48 bits of Unix milliseconds followed by 74 random bits.

The database argument for v7 is about how a B-tree index absorbs inserts. A random key scatters writes evenly across the whole key space, so consecutive inserts land on different leaf pages. The hot set becomes the entire index rather than a corner of it, the buffer cache hit rate falls, and any insert that arrives at a full page forces a split: two pages written instead of one, plus the write-ahead log, plus the parent pointer update. A key that climbs steadily does the opposite. New rows arrive at the right-hand edge, one or two pages stay hot, and writes are mostly sequential. On a write-heavy table that is the difference between an index that fits in memory and one that does not.

There is a caveat the enthusiasm for v7 tends to skip. Sorting them gives you millisecond ordering, not creation ordering, because the 74 random bits are unordered and the plain layout has no counter. Two ids minted inside the same millisecond can appear in either order. For a primary key that is fine — you wanted locality and you got it. For an audit log where you are claiming to know which event happened first, it is not, and you want either the monotonic generation method the spec describes, which spends some of those random bits on a counter, or a separate column holding the real timestamp.

Names get hashed, not generated

Versions 3 and 5 are the odd ones out: nothing about them is random, and that is their whole point. The recipe is the same for both. Take a namespace UUID, concatenate the bytes of a name onto the end, hash the result, keep the first sixteen bytes of the digest, then overwrite the version nibble with the version number and the variant bits with 10. Version 3 uses MD5. Version 5 uses SHA-1.

The namespaces are themselves ordinary UUIDs — the DNS namespace is 6ba7b810-9dad-11d1-80b4-00c04fd430c8, which reads as a version 1 with the RFC variant like any other. The consequence of hashing instead of drawing is determinism: hash the same namespace and the same name on any machine, in any language, in any year, and the same value comes back. That makes v3 and v5 the right tool when the identifier is really a name that needs a key — a row per hostname, a row per URL, a cache key derived from a request, a stable id for a record that arrives twice in a feed.

It also makes them guessable in a way the random versions are not. A v5 computed from an email address is a function of that address and a public constant, so anyone holding a list of addresses can compute the same set of ids and match them against yours. Deterministic identifiers are pseudonyms, not anonymisation. When the input comes from a small or enumerable space, so does the output.

This is where the Hash Generator genuinely belongs: run SHA-1 over the namespace bytes followed by the name bytes and you are looking at the digest a v5 is carved from, before the version and variant bits are written over the top of it.

The mistakes that bite

The arithmetic is rarely what goes wrong in practice. These are.

Building one from Math.random

Math.random() is a fast non-cryptographic generator with a small internal state. Its output is built to look uniform, not to resist being taken apart, and once an observer has collected enough consecutive values they can reconstruct that state and compute everything that follows. A random-looking identifier drawn this way is a sequence, not a secret. The fix is a line: use the platform CSPRNG. On the web that means crypto.randomUUID() for the whole value, or crypto.getRandomValues() to fill your own bytes. Check the library you already depend on, because plenty of generators predate that API.

Truncating an id and keeping the name

Cutting a v4 down to eight characters feels like a tidy way to get a shorter key. You have thrown away 90 of the 122 random bits and kept 32, which puts your even-money collision point near 77,000 rows. Worse, the characters you deleted are the ones carrying the version and the variant, so the result is no longer recognisable as a UUID by anything, including your own tooling. If you want a shorter identifier, decide how many random bits it needs first, then pick an encoding — base64url rather than hex if you care about density.

Treating an id as a secret

These values are designed to be stored, indexed, logged and displayed. They turn up in URLs, in browser history, in referrer headers, in analytics pipelines, in support tickets, and in every log line that touches the row. Putting one in a URL makes it a bearer credential whether or not you meant that. If you want a secret, generate 32 bytes from a CSPRNG, store a hash of it, and give the plaintext an expiry. A UUID that leaks is an inconvenience. A session token that leaks is an incident. And note where the danger actually sits: a version 1 value on a machine with a real hardware address has fourteen random bits and a visible node field, which is 16,384 candidates for an attacker who knows roughly when it was issued.

Sorting v4s and expecting time

Sorting a column of version 4s gives you a uniformly random permutation of your rows. It does not give you insertion order and it never will, because the bits are random by construction. People meet this the first time they sort by id and watch the newest row appear in the middle of the list. If you need the order, store a timestamp column, or move to v7.

Which version to reach for

Version Built from Random bits Guessable Sorts by time Pick it when
v1 60-bit timestamp, 14-bit clock sequence, 48-bit node 14 with a real MAC, 62 with a random node Yes, if the node is a hardware address Yes Only for interop with something that insists on it
v3 MD5 of namespace bytes plus name bytes 0 Yes, if the name is guessable No Deterministic ids where MD5 is required; otherwise prefer v5
v4 122 random bits from a CSPRNG 122 No No Anything that is an identifier and not a database key
v5 SHA-1 of namespace bytes plus name bytes 0 Yes, if the name is guessable No The same id from the same input, every time, on every machine
v6 Reordered v1: 60-bit timestamp first, then clock sequence and node 14 or 62 Yes, same as v1 Yes Ordered keys where something downstream expects the v1 field layout
v7 48-bit Unix milliseconds, then 74 random bits 74 No Yes, to the millisecond New primary keys, and anything that wants locality in an index
v8 Whatever you define, inside the field widths Up to 122 Depends on your layout Depends on your layout A custom scheme when no standard version fits
nil All 128 bits zero 0 No No As a sentinel meaning "no value", never as an identifier
MAX All 128 bits one 0 No No As the upper bound of a range query, never as an identifier

Some rows are worth reading twice. The v5 row has zero random bits and is still an excellent identifier, because it is not trying to be unpredictable, it is trying to be repeatable. The v7 row has fewer random bits than a v4 and is still more than enough for a key, because those bits are there to break ties inside a millisecond, not to make the value unguessable.

Where the tools fit. The UUID Generator lets you choose a version, stamps the version and variant nibbles for you, and shows the random-bit count for each version — so you can read 122 and 74 off the page rather than taking my word for them.

For the name-based versions, the Hash Generator computes MD5 and SHA-1 over the same bytes the spec concatenates, which is how you check a v3 or a v5 that you did not produce yourself.

The check worth running

You can audit your own identifiers in about a minute. Take the primary key of any table you own, strip the hyphens, and read the thirteenth hex digit. That is the version. If the number you find is not the version you believed you were generating, your library defaults are older than your documentation, and the correction is usually a single line at the call site.

Then apply the rule of thumb. If you are minting a key for a new table and the database writes more than it reads, use v7 and move on. If you are minting something that has to stay secret, do not use a UUID at all — draw 32 bytes from a CSPRNG, store the hash, and give it an expiry. If you need the same identifier from the same inputs on two different machines, use v5 and remember that the result is a pseudonym anyone can recompute. Keep v4 for everything that is an identifier and nothing more, because 122 random bits is an enormous number and the only way to lose it is to spend it on making the string shorter.