SecuredKit

Published: · Updated: · by

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: scattered random identifiers versus ordered time-sequenced identifiers

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 formatRandom bitsn₅₀ (theory)First duplicate, 50 runs
8 hex characters32~77,000median 81,205, earliest 9,983
6 characters, A–Z a–z 0–935.7~281,000median 349,350, earliest 24,102
10 characters, A–Z a–z 0–959.5~1.1 billionnot simulated
UUID v7 (same millisecond only)62–74~2.5 to 162 billion per msnot simulated
UUID v4122~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 behaviorv4 (random)v7 (time-ordered)
Where each row landsA random leaf pageThe rightmost (newest) page
Page splitsFrequentRare
Index fragmentationHighLow
Buffer-pool cache hitsPoor (cold pages)High (hot page reused)
Write amplificationHigh, worsens as the table growsMinimal

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, and uuid_extract_timestamp() reads the time back out.
  • SQL Server: clusters on the PK by default, but it compares uniqueidentifier values starting from the last six bytes, not the first. A v7 stored as uniqueidentifier therefore sorts by its random tail; in a 25,000-row test by dbi services it fragmented exactly like NEWID() (99.1%), while NEWSEQUENTIALID() stayed at 0.5%. On SQL Server, either keep NEWSEQUENTIALID(), or make the GUID a nonclustered key and cluster on an IDENTITY column.

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

PropertyUUID v4UUID v7
Total bits128128
Random bits12262–74
Time componentNone48-bit Unix-ms timestamp
Sortable by creationNoYes (byte order = chronological)
DB index localityPoor (random scatter)Excellent (sequential append), except SQL Server uniqueidentifier
Leaks creation timeNoYes
Predictable / enumerableNoPartially (timestamp prefix)
Standardized inRFC 4122, now RFC 9562RFC 9562 (2024)
Best forPublic IDs, unpredictabilityDatabase primary keys

Pick by use case

Your situationUse
Primary key in MySQL InnoDB or PostgreSQLv7
High-insert-rate table, want natural time orderingv7
Replacing an auto-increment INT primary keyv7
Clustered GUID key in SQL ServerNEWSEQUENTIALID(), or nonclustered GUID + IDENTITY
Public-facing ID that must not be enumerablev4
Must not leak when a record was createdv4
Distributed client-side IDs, order irrelevantv4
Security token, capability URL, or API secretNeither: 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.

Try the Free Password Generator

Generate strong, secure passwords instantly. 100% private and client-side.

Open Password Generator