Use UUIDv7 for database keys in new systems, and UUIDv4 for anything where the ID must reveal nothing, such as public tokens. Avoid v1 for new work. Collisions aren’t a real concern with any of them when generated properly. What differs in practice is what the ID leaks and how it behaves in a database index.
The current specification is RFC 9562, published in May 2024, which obsoletes the older RFC 4122 and adds versions 6, 7 and 8.
What a UUID is
A UUID is a 128-bit number, usually written as 32 hex digits in five groups:
017f22e2-79b0-7cc3-98c4-dc0c0c07398f
^ ^
| variant: 8, 9, a or b
version
The first hex digit of the third group gives the version. The two top bits of the fourth group give the variant. That makes 6 bits fixed, and 122 available. To find a UUID’s version, look at that one digit. The example above, taken from the RFC’s appendix, is a v7.
The versions
| Version | Built from | Sortable by time? | Leaks |
|---|---|---|---|
| v1 | 60-bit timestamp, clock sequence, 48-bit node | Not as bytes | Creation time and possibly a MAC address |
| v3, v5 | MD5 or SHA-1 hash of a namespace and name | No | Nothing directly |
| v4 | 122 random bits | No | Nothing |
| v6 | v1’s fields, reordered | Yes | As v1 |
| v7 | 48-bit Unix milliseconds, then random bits | Yes | Creation time |
| v8 | Custom, vendor-defined | Depends | Depends |
v1 was designed for uniqueness across machines without coordination, using the network card’s address as the node. RFC 9562 allows a random node value, but older generators exposed real hardware addresses. The timestamp’s low bits come first, so v1 values don’t sort by time. The RFC says plainly that “Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible.”
v3 and v5 are deterministic: the same namespace and name always give the same UUID. That’s their use, for instance deriving a stable ID from a URL. Prefer v5, though its SHA-1 use means you shouldn’t rely on it for security.
v4 is just random bits. In most languages, it’s what randomUUID() gives you.
v7 is the newest general-purpose version:
| 48 bits | 4 | 12 bits | 2 | 62 bits |
| unix_ts_ms | ver | rand_a | var | rand_b |
The RFC’s example begins 017f22e279b0. Read as milliseconds since 1970, that’s 2022-02-22 19:22:22 UTC, which we confirmed by decoding it. So anyone holding the ID can read its creation time, to the millisecond. The 48-bit field lasts until around the year 10889.
The collision math, correctly
For v4, there are 2^122 possible values, about 5.3 × 10^36. The probability of at least one collision among n random values is close to the birthday approximation:
p ≈ 1 - e^(-n² / (2 × 2^122))
We calculated these values:
| UUIDs generated | Chance of any collision |
|---|---|
| 1 million | 9.4 × 10^-26 |
| 1 billion | 9.4 × 10^-20 |
| 1 trillion | 9.4 × 10^-14 |
| 103 trillion | about 1 in a billion |
| 1 quadrillion | 9.4 × 10^-8 |
| 2.71 quintillion (2.71 × 10^18) | about 50% |
At one billion UUIDs per second, it would take about 86 years to reach the 50 percent mark.
The real risk is a bad random source, not the math. Using a non-cryptographic generator, or virtual machines restored from one snapshot with identical random state, can produce duplicates. Use your platform’s cryptographic generator, and keep a unique constraint on the column, because a constraint turns a once-in-a-universe event into an ordinary error rather than corrupted data.
v7 is a different calculation. It has 74 random bits, but only IDs created in the same millisecond can collide. If you generate one million IDs in a single millisecond, the chance of a collision in that millisecond is about 2.6 × 10^-11, by the same formula. RFC 9562 also describes methods that use some random bits as a counter, keeping order within a millisecond. Python’s uuid.uuid7() uses a 42-bit counter, for example.
Database index implications
This is why the choice matters. A B-tree primary key index stays sorted. Random keys land in random spots, whereas increasing keys always go at the end.
With random v4 keys:
- Each insert touches a random leaf page, so the working set of the index is close to its entire size.
- Pages split when full, leaving them partly empty, and they fragment over time.
- In engines that cluster rows by primary key, such as MySQL’s InnoDB, the rows themselves get scattered, so neighbouring records aren’t stored near each other. Secondary indexes also carry the key, so its 16 bytes get repeated.
With v7, new keys land near the end of the index, like an auto-increment. The RFC notes that “Time-ordered monotonic UUIDs benefit from greater database-index locality”.
How big is the difference? It depends. We inserted 2 million rows into a SQLite table keyed by a 16-byte BLOB, on a Mac with a 20 MB page cache. v4 took 37 seconds, and v7 took 30 seconds, with database files of about the same size (55.7 and 55.9 MB). This includes the time spent generating IDs in Python, and the data fit in the operating system’s file cache. So don’t expect magic on a small table. The gap grows when the index no longer fits in memory and random inserts become random disk reads, which our test didn’t reach. Run your own test at your own scale and on your own database.
Storage advice holds regardless. Store a UUID in a native type (uuid in PostgreSQL) or as BINARY(16) in MySQL, not as a 36-character string, which more than doubles the size of each key and of every index that contains it.
Generating v7 in practice
| Platform | How |
|---|---|
| PostgreSQL 18+ | uuidv7() built in, alongside uuidv4() |
| Python 3.14+ | uuid.uuid7() in the standard library |
| Others | A maintained library, until your platform adds it |
We ran uuid.uuid7() on Python 3.14 and got 01a19f2f-b660-74ef-8e5c-7d974385861c. As the version digit shows, it’s a v7, and generating two in a row shows them sorted.
Using our generator
Our UUID Generator makes v4 or v7, up to 1,000 at a time, using the browser’s cryptographic random source, in hyphenated, uppercase, no-hyphen and braced formats. UUIDs are case-insensitive by specification, so the uppercase and lowercase forms name the same value, though a database comparing them as text won’t agree.
Use it for test data, seed rows and one-off identifiers. For production keys, generate them in your database or application so that they’re created along with the row. To read the time from a v7 ID, take the first 12 hex digits, convert them to decimal with the Number Base Converter, and paste the result into the Unix Timestamp Converter, which accepts milliseconds.
When to use what
- Primary keys in a new database: v7. It’s sortable, works well in indexes, and can be generated by the client or by any service without coordination.
- Public IDs where creation time is sensitive: v4. A v7 in a URL tells anyone when the account or document was created, and that can reveal growth rate or link a user to an event. A common design uses v7 internally and exposes a separate v4.
- Session tokens and password reset links: Not a UUID at all. Use a dedicated random token from a cryptographic generator, with 256 bits if you can. RFC 9562’s security section warns against assuming UUIDs are hard to guess.
- IDs derived from a name: v5.
- Existing v4 systems: Changing rarely pays off unless you’ve measured a real index problem. New tables can adopt v7 without touching old ones, since all versions fit the same 128-bit column.
- Auto-increment integers are still a reasonable choice. They’re smaller and faster for a single database. UUIDs earn their place when IDs must be created in many places or merged later, or when you don’t want sequential IDs to expose your row count.