UUIDs Explained
A UUID looks like a random smear of hex — f47ac10b-58cc-4372-a567-0e02b2c3d479 — but there is real structure hiding in that string. Underneath it is a single 128-bit number, and two of its digits are not random at all: they encode how the identifier was built. This guide takes a UUID apart from first principles: the 16 bytes behind the 8-4-4-4-12 layout, exactly where the version and variant live, a field-by-field dissection of one concrete value, the honest difference between v4 and v7, what the collision odds really are (astronomically unlikely, not magically impossible), and the standing question of when a UUID beats a plain auto-increment integer — and when it does not.
What a UUID actually is
A UUID — Universally Unique Identifier, called a GUID on Microsoft platforms — is a 128-bit number. That is the entire substance of it. One hundred and twenty-eight bits is 16 bytes, and the whole point of the format is to make that number so large, and so well spread out, that any two systems can each invent one without coordinating and still be safe to assume they will never clash.
The string you actually see is just a human-readable rendering of those 16 bytes. Each byte is two hexadecimal digits, so 16 bytes become 32 hex digits. Those 32 digits are then grouped 8-4-4-4-12 and joined with four hyphens, giving the canonical 36-character text form. The hyphens are pure punctuation — they carry no information and exist only to make the value easier to read and to copy in chunks. Strip them and you still have the same identifier; the number of digits, 32, is what is fixed.
So there are three ways to hold the same UUID: as 16 raw bytes (how a database should store it), as 32 hex digits, or as the 36-character hyphenated string (how logs and APIs show it). None of them is "more" of a UUID than the others — they are the same 128 bits wearing different clothes.
The two digits that are not random
Not all 128 bits are free. A modern UUID reserves a handful to say what kind of identifier it is, and those bits surface as two specific positions in the string:
- The version lives in the 13th hex digit — the first digit of the third group. In the shape
xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx, thatVis the version nibble:4for a random UUID,7for a time-ordered one, and1,3or5for the older schemes. One digit tells you how every other bit was produced. - The variant lives in the top bits of the 17th hex digit — the first digit of the fourth group. It marks which family of layout rules the UUID follows. For essentially everything you meet today (the RFC 4122 / RFC 9562 variant) the top two bits are
10, which forces that digit to be one of8,9,aorb. If the fourth group does not start with one of those four characters, you are not looking at a standard UUID.
Everything else — 122 bits in a v4, fewer in the versions that spend bits on a timestamp — is payload. Knowing just these two positions lets you glance at any UUID and read its version and confirm its variant without a tool.
Worked example: dissecting one UUID
Take this real version-7 value from the RFC 9562 specification and pull it apart field by field:
Read left to right: the first 48 bits (the whole first group plus the second group) are a Unix timestamp in milliseconds — 0x017f22e279b0 is 1,645,557,742,000, which decodes to a moment in February 2022. Then comes the version digit 7. The 9 that opens the fourth group is 1001 in binary; its leading 10 is the variant marker, and the remaining bits are random. Add it up and a v7 carries a 48-bit time prefix, 4 version bits, 2 variant bits, and 74 bits of randomness. A v4 is the same skeleton with the timestamp replaced by randomness too: 122 random bits, minus only the 4 version and 2 variant bits that are pinned.
v4 versus v7: opaque or sortable
Two versions are worth generating fresh today, and the choice between them comes down to a single question: do you want the identifier to reveal nothing, or do you want it to sort by time?
| v4 — random | v7 — time-ordered | |
|---|---|---|
| Built from | 122 random bits | 48-bit ms timestamp + 74 random bits |
| Sorts by creation time? | No — fully scattered | Yes — roughly, lexically |
| Leaks when it was made? | No | Yes — to the millisecond |
| Best for | Opaque external ids, tokens | Database primary keys |
v4 is nothing but randomness. Consecutive values are unrelated, and the string betrays neither when nor where it was minted — that opacity is exactly what you want for an identifier a stranger might see. v7, standardised in RFC 9562 in May 2024, trades a slice of that randomness for a leading millisecond timestamp. Because the time sits in the high-order bits, sorting v7 strings lexically also sorts them by creation order. That one property is what makes v7 so friendly to database indexes — and the same property is its one privacy cost, because anyone holding a v7 can read the approximate moment it was created.
How unlikely is a collision, honestly?
A v4 UUID draws from 122 random bits, so there are 2122 — about 5.3 × 1036 — possible values. Uniqueness is not guaranteed by a registry or a counter; it is a probabilistic bet that two independent draws from that space will never coincide. The right way to size that bet is the birthday bound: the number of values you can generate before the chance of any pair matching reaches about 50% is roughly the square root of the space, near 261 ≈ 2.6 × 1018 UUIDs. To put that in scale, generating a billion v4 UUIDs every second, you would pass that 50% mark only after somewhere on the order of 80–90 years of non-stop work.
The honest framing matters here: this is astronomically unlikely, not literally impossible. Anyone quoting a precise "one collision every N seconds" figure is dressing up the same birthday-bound estimate as if it were a measured rate — treat those claims with suspicion. And the whole calculation rests on one assumption: that the bits are drawn from a genuine cryptographic random source. A UUID library built on a weak generator (a seeded Math.random, say) can repeat or become predictable long before the maths says it should, which quietly voids both the collision odds and any unguessability you were relying on.
UUID versus an auto-increment integer
The classic alternative to a UUID primary key is a plain auto-incrementing integer — 1, 2, 3, … handed out by the database. Both identify a row uniquely; they differ in everything else, and the trade-offs are real in both directions:
| UUID | Auto-increment integer | |
|---|---|---|
| Who can generate it | Anyone, anywhere, offline | Only the database, in sequence |
| Size | 16 bytes | 4 or 8 bytes |
| Reveals record count / order? | No (v4) / time only (v7) | Yes — trivially enumerable |
| Natural ordering | None (v4) / by time (v7) | Insertion order, for free |
An integer is compact, naturally ordered, and cheap to index — but it forces every insert to ask the database for the next number, and it broadcasts information: expose /orders/1042 in a URL and you have told the world roughly how many orders exist and invited anyone to try /orders/1041. A UUID fixes both problems. It can be minted on a phone, in a background worker, or across several database shards with no central counter and no round-trip, and a v4 gives away nothing about volume or neighbours. The price is size — 16 bytes against 4 or 8 — and, for v4, the loss of natural ordering that made integer inserts index-friendly. That last point is precisely the gap v7 was designed to close: it keeps the distributed, opaque-ish generation of a UUID while restoring the roughly-sequential ordering that keeps a B-tree index compact.
Choosing in practice
A short decision rule covers most cases. Reach for v4 when the identifier will be seen from outside — public IDs, share tokens, anything you never want to reveal timing or ordering. Reach for v7 when the identifier is mainly a storage key and index locality matters, and the mild leak of creation time is acceptable. Reach for a plain auto-increment integer when generation is always centralised, space and raw index speed are at a premium, and enumeration is not a concern — an internal lookup table, say. And keep the older versions in mind only for reading legacy data: v1 embeds a timestamp and historically a MAC-derived node id (so it leaks host and time), while v3 and v5 are the deterministic, name-based schemes that always turn the same input into the same UUID.
Mint v4, time-ordered v7 or the Nil UUID in bulk — up to 100 at once, with uppercase, no-hyphen and brace options — entirely in your browser. Nothing is sent anywhere.
Putting it together
A UUID is one 128-bit number — 16 bytes, 32 hex digits, four cosmetic hyphens. Two positions carry meaning: the 13th digit is the version and the 17th holds the variant, and everything else is payload. v4 spends that payload on 122 random bits for an opaque, unordered identifier; v7 spends the first 48 on a millisecond timestamp so the value sorts by time, at the cost of leaking when it was made. Collisions are governed by the birthday bound — vanishingly unlikely across 2122 values, but only with real cryptographic randomness, and never "impossible" in the strict sense. Weighed against an auto-increment integer, a UUID buys distributed generation and enumeration-resistance and pays in bytes and (for v4) ordering. Keep those few facts straight and the random-looking smear becomes something you can read at a glance.
Open the UUID Generator → to mint v4 or v7 values and inspect the version and variant digits yourself, or browse all Polyatic tools.