Published: · Updated: · by kodewright
UUID v4 vs v7: When to Use Each (2026 Guide)
UUID v4 is 122 random bits; v7 starts with a millisecond timestamp so database indexes stay sequential. Bit layout, a collision test and which to pick.

UUID v4 vs v7 comes down to one choice: v4 is fully random, v7 starts with a timestamp. Use v7 for database primary keys, because its time-ordered prefix keeps inserts sequential. Use v4 when an ID must leak nothing about when it was created. Both are 128 bits, and neither will collide at any realistic scale.
v4 was the default for years, and in a clustered index it costs write performance on every insert. RFC 9562 (May 2024) standardized v7 to fix that. SQL Server is the one common database where v7 doesn't help, for a reason covered below.
The 128 bits, decoded
Every UUID is 128 bits, and the version decides how they're spent:
UUID v4 — 128 bits, 122 of them random
┌──────────────────────────┬──────┬──────────────────────────┐
│ random (48 bits) │ ver │ random + variant │
│ ████████████████████████ │ 0100 │ ██ variant 10 ██████████ │
└──────────────────────────┴──────┴──────────────────────────┘
no time, no order — every bit is noise except 6 fixed ones
UUID v7 — 128 bits, time-ordered
┌────────────────────────┬──────┬────────┬──────┬────────────┐
│ unix_ms timestamp (48) │ ver │ rand 12│ var │ random 62 │
│ 1716800000000… │ 0111 │ ▓▓▓▓ │ 10 │ ██████████ │
└────────────────────────┴──────┴────────┴──────┴────────────┘
first 48 bits = creation time → sorts chronologically
The difference is in the first 48 bits. v4 fills them with noise; v7 puts a Unix timestamp in milliseconds there and leaves the rest random. That turns the identifier from scattered into sequential, which is the property a database index cares about.
Both share 6 fixed bits: a 4-bit version field (0100 for v4, 0111 for v7) and a 2-bit variant field (10). The 12 bits marked rand 12 don't have to be random: RFC 9562 lets an implementation use them for a sub-millisecond time fraction or a counter, so that IDs created in the same millisecond still sort in order. PostgreSQL 18's uuidv7() uses them as a sub-millisecond timestamp fraction.
Short random IDs collide, UUIDs don't
I've had this bug myself: a project used short random IDs, and two of them came out identical. Short IDs feel random enough because each one looks unique, but collisions depend on how many IDs exist in total, not on any single one. That's the birthday problem: for b random bits, a first collision becomes a coin flip at roughly
n₅₀ ≈ 1.177 × √(2^b)
where n₅₀ is the number of IDs generated so far. The square root is what bites. Doubling the length of an ID squares the safe count; a 32-bit ID gives you only about 77,000 IDs before the odds of a duplicate pass 50%.
To check the formula against something real, I generated IDs with Node 22's crypto.randomBytes and crypto.randomInt until the first duplicate appeared, 50 runs per format:
import { randomBytes } from 'node:crypto'
function idsUntilCollision(gen) {
const seen = new Set()
for (;;) { const id = gen(); if (seen.has(id)) return seen.size + 1; seen.add(id) }
}
idsUntilCollision(() => randomBytes(4).toString('hex')) // 8 hex chars = 32 bits
| ID format | Random bits | n₅₀ (theory) | First duplicate, 50 runs |
|---|---|---|---|
| 8 hex characters | 32 | ~77,000 | median 81,205, earliest 9,983 |
| 6 characters, A–Z a–z 0–9 | 35.7 | ~281,000 | median 349,350, earliest 24,102 |
| 10 characters, A–Z a–z 0–9 | 59.5 | ~1.1 billion | not simulated |
| UUID v7 (same millisecond only) | 62–74 | ~2.5 to 162 billion per ms | not simulated |
| UUID v4 | 122 | ~2.7 × 10¹⁸ | not simulated |
The "earliest" column is the part that matters in production: one run out of 50 hit a duplicate after fewer than 10,000 eight-character IDs. A table with a few thousand rows is already in the danger zone if nothing enforces uniqueness.
The UUID rows are a different world. v4's 2.7 × 10¹⁸ means generating a billion IDs per second for about 86 years. v7 has fewer random bits, but its timestamp separates IDs from different milliseconds, so only IDs created in the same millisecond can collide; even with the 12 rand_a bits spent on a sub-millisecond clock, that's about 2.5 billion IDs inside one millisecond. Neither will collide at a real insert rate. If you need short IDs for URLs, keep a unique constraint on the column and retry on conflict, because the math above says you will eventually need it.
Where the random bits come from matters as much as how many there are. A library that fills them with Math.random() gets V8's xorshift128+, whose internal state can be recovered from a handful of outputs, after which every later ID is predictable. crypto.randomUUID() in the browser uses the same CSPRNG as crypto.getRandomValues(); check that your server-side library does the same.
Why v4 hurts your database index
Primary keys live in a B-tree index, and B-trees handle sequential inserts best. A random v4 key lands in a random leaf page every time:
v4 — a random key lands mid-page, so a full page splits in two
┌───────────────┐ ┌─────────┐ ┌───────┐
│ page: 3 5 8 9 │ + insert 6 → │ 3 5 6 │ │ 8 9 │ ◄ SPLIT
└───────────────┘ └─────────┘ └───────┘
…and this repeats for nearly every insert, all over the tree
v7 — a time-ordered key always lands at the right edge, no split
┌────────────────┐ ┌────────────────┐
│ page: …97 98 │ + insert 99 → │ …97 98 99 │ ◄ just appends
└────────────────┘ └────────────────┘
pages fill densely, writes stay sequential, the hot page stays cached
With v4, each insert can touch a cold page anywhere in the tree, forcing a read before the write, and full pages get split to make room. The result is index fragmentation, poor buffer-pool locality, and write amplification that grows with the table.
| Index behavior | v4 (random) | v7 (time-ordered) |
|---|---|---|
| Where each row lands | A random leaf page | The rightmost (newest) page |
| Page splits | Frequent | Rare |
| Index fragmentation | High | Low |
| Buffer-pool cache hits | Poor (cold pages) | High (hot page reused) |
| Write amplification | High, worsens as the table grows | Minimal |
The cost is highest in clustered-index engines, where the primary key is the physical row order:
- MySQL InnoDB: the clustered PK dictates the on-disk layout. Random v4 keys are the worst case, and v7 fixes it. MySQL has no built-in v7 function, so generate it in the application.
- PostgreSQL: tables are heaps, so the PK is a secondary index. The penalty is milder, but v7 still improves index locality. PostgreSQL 18 ships
uuidv7()natively, anduuid_extract_timestamp()reads the time back out. - SQL Server: clusters on the PK by default, but it compares
uniqueidentifiervalues starting from the last six bytes, not the first. A v7 stored asuniqueidentifiertherefore sorts by its random tail; in a 25,000-row test by dbi services it fragmented exactly likeNEWID()(99.1%), whileNEWSEQUENTIALID()stayed at 0.5%. On SQL Server, either keepNEWSEQUENTIALID(), or make the GUID a nonclustered key and cluster on anIDENTITYcolumn.
v7 fixes inserts, with one catch
v7 gives you the global uniqueness of a UUID with the insert pattern of an auto-increment integer. Because the high 48 bits are a timestamp, byte order equals creation order, and you get a rough "created at" for free.
The catch: v7 leaks the creation time. Anyone holding the ID can read the embedded millisecond timestamp, and can predict the high bits of IDs created around the same moment. That's fine for an internal primary key and a problem for a public identifier:
- Don't use v7 in a capability URL (a "secret link"). The timestamp narrows the guess space and reveals metadata.
- Don't use v7 where creation-time disclosure is sensitive (medical records, financial events, anything you'd redact in a log).
For those cases, v4's wall of noise is the feature. For real secrets (API keys, session tokens, signing secrets), neither version is the right tool. A UUID tops out at 122 random bits and has a fixed, recognizable format; a 256-bit value straight from a CSPRNG has no structure to leak.
For v4 UUIDs right now, my UUID Generator calls crypto.randomUUID() in your browser tab, so the IDs are never sent to a server. It makes v4 only, not v7. It generates up to 100 at once if you're seeding test data.
v4 vs v7: the full comparison
| Property | UUID v4 | UUID v7 |
|---|---|---|
| Total bits | 128 | 128 |
| Random bits | 122 | 62–74 |
| Time component | None | 48-bit Unix-ms timestamp |
| Sortable by creation | No | Yes (byte order = chronological) |
| DB index locality | Poor (random scatter) | Excellent (sequential append), except SQL Server uniqueidentifier |
| Leaks creation time | No | Yes |
| Predictable / enumerable | No | Partially (timestamp prefix) |
| Standardized in | RFC 4122, now RFC 9562 | RFC 9562 (2024) |
| Best for | Public IDs, unpredictability | Database primary keys |
Pick by use case
| Your situation | Use |
|---|---|
| Primary key in MySQL InnoDB or PostgreSQL | v7 |
| High-insert-rate table, want natural time ordering | v7 |
| Replacing an auto-increment INT primary key | v7 |
| Clustered GUID key in SQL Server | NEWSEQUENTIALID(), or nonclustered GUID + IDENTITY |
| Public-facing ID that must not be enumerable | v4 |
| Must not leak when a record was created | v4 |
| Distributed client-side IDs, order irrelevant | v4 |
| Security token, capability URL, or API secret | Neither: use a 256-bit CSPRNG secret |
What about v1, v6 and v8?
RFC 9562 (which obsoleted RFC 4122) defines a few more versions you'll run into:
- v1: timestamp plus the machine's MAC address. Time-based, but it leaks a hardware identifier, and its timestamp fields are stored low-bits-first, so it doesn't sort. The RFC says to prefer v7 over v1 and v6 where possible.
- v6: v1 with the timestamp fields reordered so it sorts. A reasonable target if you're already on v1; for new systems v7 is simpler.
- v8: an explicitly custom version. The spec fixes only the version and variant bits and lets you define the rest, for when you need to embed your own structured data.
For a new system you'll almost always pick between v7 for keys and v4 for anything that must stay opaque.
Storage and implementation notes
A UUID is 16 bytes. Stored as the canonical 36-character string (CHAR(36)), it takes more than twice the space and slows every index comparison. Store it as BINARY(16) in MySQL or the native uuid type in PostgreSQL: smaller pages, faster comparisons, denser indexes.
-- MySQL: store the 16 raw bytes, not the 36-char string
id BINARY(16) PRIMARY KEY
-- PostgreSQL 18+: native uuid type with a built-in v7 default
id uuid PRIMARY KEY DEFAULT uuidv7()
If a legacy InnoDB table uses v1 keys, MySQL's UUID_TO_BIN(uuid, 1) swaps the time fields to the front so they sort. Don't apply that swap flag to v7 (it's already in order) or to v4 (there's no time to move). A table full of v4 keys can't be fixed in place; switch new rows to v7, and the old rows simply stay scattered.
Where to go from here
If you're choosing an ID scheme today: v7 for the primary key (unless you're on SQL Server), v4 from the UUID Generator for anything public, and a 256-bit hex or base64 value from the Secret Key Generator for anything that has to stay secret. The randomness behind all three is explained in how CSPRNGs work.
FAQ
Is UUID v7 safe to use as a security token?
No. The timestamp prefix is readable and predictable for IDs created around the same time. Use v4 for public IDs that shouldn't be guessable, and a 256-bit secret from a CSPRNG for real tokens.
Does UUID v7 replace v4?
No. RFC 9562 recommends v7 over v1 and v6, not over v4. v7 is the better default for primary keys, v4 stays the right choice for externally exposed IDs that shouldn't leak when they were created.