The Complete Guide to UUIDs: v1–v7, Collision Math, and Database Reality
2026-08-06
Open the database of any modern system and you’ll likely find strings like 0198a3e2-7b1c-7f3a-9e4d-2c5b8a1f6e90 sitting in primary key columns. That’s a UUID — a universally unique identifier. It looks like random keyboard mashing, but every bit is governed by a spec: the version number sits at a fixed position, the leading digits may encode a timestamp, and the collision guarantee rests on a classic birthday-paradox calculation. This guide walks through versions v1 to v7, the math behind the uniqueness promise, and the mistakes people keep making with UUIDs in production.
The Anatomy: 128 Bits, Five Segments
A UUID is a 128-bit (16-byte) integer conventionally written as hexadecimal in an 8-4-4-4-12 pattern — 36 characters including the four hyphens:
0198a3e2-7b1c-7f3a-9e4d-2c5b8a1f6e90
↑ ↑
version variant
Two positions are worth memorizing: the first digit of the third segment is the version (here 7, meaning v7), and the first digit of the fourth segment is the variant (8, 9, a, or b for standard RFC UUIDs). So identifying a UUID’s version takes one glance at the start of the third group. If you’d rather not decode by eye, paste it into the UUID Decoder — it labels the version, the embedded timestamp (for time-based versions), and what each field means. Everything else in the 128 bits depends on the version.
v1 Through v7: What Each Version Is For
v1 (timestamp + MAC address): the original. It concatenates the current time (in 100-nanosecond ticks since 1582 — the Gregorian calendar reform date, a genuine spec quirk), a clock sequence, and the generating machine’s MAC address. It’s ordered and practically collision-free, but it leaks your machine’s MAC address and the exact creation time, and clock skew across machines can bite you. A typical v1 looks like c2f4d8a0-6e3b-11ec-90d6-0242ac120003 — note the third segment starts with 1.
v3 (MD5 name-based): hashes a namespace plus a name with MD5 to produce a deterministic UUID. Same input, same UUID, forever — useful for deriving stable IDs for known entities. MD5 is cryptographically broken, so v3 mostly shows up when maintaining legacy code.
v4 (pure random): the most widely deployed version. 122 bits are entirely random (128 minus the 4 version bits and 2 variant bits). Example: f47ac10b-58cc-4372-a567-0e02b2c3d479 — the 4 in the third group gives it away. Simple to generate, leaks nothing, which explains its popularity. The cost is total randomness, which databases don’t love — more on that below.
v5 (SHA-1 name-based): v3’s successor, swapping MD5 for SHA-1 with identical semantics: same namespace plus same name always yields the same UUID. If you need “hash this email into a stable ID,” v5 is the right tool — don’t hand-roll your own scheme.
v6 (reordered v1): rearranges v1’s timestamp fields so the most significant bits come first, making lexicographic order match chronological order. Think of it as the index-friendly v1, mostly used for upgrading systems that already have v1 data.
v7 (time-ordered + random, standardized in RFC 9562 in 2024): the first 48 bits are a Unix timestamp in milliseconds, followed by 74 random bits. This combines v1’s monotonic ordering with v4’s statelessness and privacy. The leading groups of 0198a3e2-7b1c-... literally are the millisecond count. To see the contrast yourself, generate a batch of each in the UUID Generator — v4s land all over the place while v7s share a growing prefix. For new systems, v7 as the primary key is close to a community consensus.
Collision Probability: The Birthday Paradox
“v4 is random — could two ever collide?” Yes, but you won’t live to see it. 122 random bits means a space of 2^122 ≈ 5.3 × 10^36 values. But collisions don’t wait until you’ve enumerated the space — the birthday paradox says they arrive far earlier than intuition suggests. Among just 23 people there’s a 50% chance two share a birthday, even with 365 days available, because the number of pairs grows quadratically.
Plugging UUIDs into the same formula: you need roughly 2.71 × 10^18 (about 2.71 quintillion) v4 UUIDs before the probability of a single collision reaches 50%. At a sustained rate of one billion UUIDs per second for a full century, you’d generate about 3 × 10^18 — barely at the coin-flip line. A more practical framing: generating 10 trillion UUIDs carries roughly a one-in-a-billion chance of one collision. That’s below the risk profile of hardware failures you already ignore. What actually causes collisions in the wild is a broken random source — using Math.random() instead of a CSPRNG, or cloning VM snapshots that duplicate the RNG state. The rule is simple: use your standard library (Python’s uuid.uuid4(), crypto.randomUUID() in browsers, Java’s UUID.randomUUID()) and never roll your own randomness.
Database Indexes: Random vs. Time-Ordered
This is where the version choice has real, measurable impact. B-tree indexes (and InnoDB’s clustered primary key is one) reward sequential inserts: new values append at the right edge, pages split rarely, writes stay fast. Auto-increment integers and v7 UUIDs both follow this append-friendly pattern.
v4 is uniformly random: every new row lands at an arbitrary position in the index tree. That means poor buffer-pool locality, frequent page splits, index fragmentation, and noticeably degraded write throughput as tables grow. This is the core of the case against v4 primary keys. One widely used mitigation regardless of version: store the UUID as BINARY(16) instead of CHAR(36). Storage drops from 36 to 16 bytes, the index shrinks by more than half, and comparisons get faster.
The practical conclusion: new systems should use v7 stored as binary; existing v4 systems under modest write load don’t need to migrate. PostgreSQL 18 ships a built-in uuidv7() function, and mainstream UUID libraries in every major language have added v7 support.
The Classic Misuse: UUIDs Are Not Secrets
This deserves its own section: a UUID is an identifier, not a credential. “122 random bits, nobody can guess it” is true for IDs and false for tokens, for three reasons:
- UUIDs end up in logs, URLs, referer headers, and error messages — the leak surface is bigger than you think.
- Not every UUID comes from a cryptographically secure source — you can’t control how that UUID in a third-party system was generated.
- Tokens need to be revocable, expirable, and rotatable. A UUID has no lifecycle semantics at all.
For password reset links, API keys, and session credentials, use dedicated random tokens (e.g., 256 bits from a CSPRNG, base64url-encoded) with expiry and revocation — not a v4 you generated in one line. This is the same tier of common sense as “don’t store passwords with MD5 or SHA-1”; if you ever need to sanity-check hash algorithm choices, the Hash Generator makes the outputs easy to compare side by side.
A Quick Decision Checklist
- Primary key in a new system: v7, stored as
BINARY(16). - Ephemeral IDs that never hit a database (frontend temp nodes, trace IDs): v4 is fine and simplest to generate.
- Deterministic IDs derived from names (same input → same ID): v5.
- v1/v3/v6 encountered in legacy code: recognize them, don’t introduce them in new code.
- Mystery UUID you want to identify: drop it in the UUID Decoder — version and (for v1/v7) the embedded timestamp come back immediately.
- Anything security-sensitive: not a UUID — a proper cryptographic token.
The genius of UUIDs is that they turn “globally unique” into a local operation — no central coordinator, no auto-increment lock, any machine at any time. Once you understand the version differences, the collision math, and the index behavior, choosing one is just pattern-matching to your scenario.