Skip to content
Developer toolsIdentifiersDatabases

UUID v4 or v7 for database keys

Random v4 keys scatter writes across a B-tree and slow inserts as tables grow. v7 sorts by time and fixes that, but leaks creation time. Use v7 internally, v4 in public.

Ashish S Kumar5 min read

Reaching for a UUID as a primary key is usually the right instinct — you can mint one on the client, merge two datasets without collisions, and never expose how many rows a table holds. Reaching for version 4 specifically is the part worth reconsidering, because since RFC 9562 there is a version designed for exactly this job and v4 is not it.

What changed

RFC 9562 replaced the long-standing RFC 4122 and added versions 6, 7 and 8. Version 7 is the interesting one: the first 48 bits are a Unix timestamp in milliseconds, and the remaining bits are random. That single rearrangement makes the identifiers sort in creation order while staying practically impossible to guess.

v4v7
Structure122 random bits48-bit millisecond timestamp, then 74 random bits
Sorts by creation timeNoYes, lexicographically and as bytes
Reveals when it was madeNoYes, to the millisecond
Index locality on insertPoor — writes land anywhereGood — writes land at the end
Built into crypto.randomUUID()YesNo, needs a library or your own

Generate either version, in bulk

Runs in your browser — nothing is uploaded. Open the full uuid generator

Why random keys hurt a database

Most relational databases store the primary key in a B-tree, and many — InnoDB among them — physically cluster the row data in that key's order. Insert rows with sequential keys and every write lands at the right-hand edge of the tree, in pages already sitting in memory. Insert rows with random keys and each write targets an unpredictable page, so the database keeps pulling cold pages in, dirtying them, and splitting them when they fill.

The effect is small on a table of ten thousand rows and very much not small on a table of fifty million. It shows up as write throughput degrading as the table grows, an index noticeably larger than it ought to be, and a buffer pool that never seems big enough.

Version 7 removes the cause. Because the leading bits are a timestamp, identifiers generated close together in time are close together in the index, and the insert pattern looks much more like an auto-incrementing integer while keeping everything that made you want a UUID in the first place.

The case against v7, which is real

A v7 identifier tells anyone holding it when the row was created, to the millisecond. Usually that is harmless. Sometimes it is not.

  • Two identifiers from the same request are visibly adjacent, so a sequence of them reveals ordering and rough rate.
  • A user-visible identifier — in a URL, a filename, a support reference — hands out the creation timestamp of that record to anyone who looks.
  • Sampling a handful of public identifiers gives a reasonable estimate of how fast the table is growing, which is occasionally commercially sensitive.

None of that makes a v7 guessable — there are still 74 random bits, so enumeration is not on the table. It is a metadata leak, not an authentication weakness. If you want to read the timestamp out of one you already have, the first 48 bits are plain milliseconds since the epoch; the timestamp converter turns that into a date.

Read the epoch value back as a dateEpoch to date and back, in seconds or milliseconds.

The position

Use v7 for internal primary keys and v4 for anything a stranger sees.

That split costs almost nothing to implement and resolves both concerns at once. Your tables get the insert locality they want; your public URLs and tokens carry no timestamp. Where a record needs both — an internal key and an external reference — store two columns rather than compromising on one identifier that does neither job well.

Use v7 whenUse v4 when
It is a primary or foreign keyIt appears in a URL a user can see
The table will get largeIt is a share link, invite code or reset token
Rows are frequently read in creation orderCreation time is sensitive
It never leaves your infrastructureIt is handed to a third party

One caveat that applies to both: a UUID is 128 bits. Stored as a native `uuid` or `binary(16)` column that is fine. Stored as a 36-character string — which is what happens by default in more schemas than anyone would like — it is 36 bytes plus overhead, repeated in every foreign key and every index that includes it. Check the column type before blaming the identifier.

Getting them safely

`crypto.randomUUID()` is built into browsers and Node and returns a v4 drawn from a cryptographically secure source. There is no equivalent built-in for v7 yet, so it comes from a library or a short implementation of your own. Whichever route you take, the random bits must come from a CSPRNG — `crypto.getRandomValues`, not `Math.random`. A UUID built from `Math.random` looks identical and is predictable enough to enumerate.

Frequently asked questions

Is UUID v7 safe to use yet?
Yes. It is standardised in RFC 9562, published in 2024, and supported by libraries across every major language. Some databases also now generate it natively. It is no longer a draft proposal.
Can someone guess a v7 UUID from another one?
No. The timestamp portion is predictable by design, but 74 bits remain random. Guessing a specific identifier is computationally infeasible. What a v7 does leak is when the record was created, which is a privacy consideration rather than a security hole.
Should I migrate existing v4 keys to v7?
Usually not. Rewriting primary keys across a live schema is a large, risky change for a benefit that mostly accrues to future inserts. Switch new tables and new rows to v7 and leave the existing ones alone unless you have measured a specific problem.
Is a UUID better than an auto-incrementing integer?
It is a trade. Integers are smaller and naturally ordered but require a round trip to the database to obtain, collide when merging datasets, and expose row counts. UUIDs solve all three at the cost of size — and, if you pick v4, insert performance.
What is the nil UUID for?
It is all zeros and represents the absence of a value in systems where the column cannot be null. Useful as an explicit sentinel, but a nullable column is usually clearer if the schema allows one.

Sources

  1. 1.RFC 9562 — Universally Unique IDentifiers (UUIDs) — IETF