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.
| v4 | v7 | |
|---|---|---|
| Structure | 122 random bits | 48-bit millisecond timestamp, then 74 random bits |
| Sorts by creation time | No | Yes, lexicographically and as bytes |
| Reveals when it was made | No | Yes, to the millisecond |
| Index locality on insert | Poor — writes land anywhere | Good — writes land at the end |
| Built into crypto.randomUUID() | Yes | No, 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 when | Use v4 when |
|---|---|
| It is a primary or foreign key | It appears in a URL a user can see |
| The table will get large | It is a share link, invite code or reset token |
| Rows are frequently read in creation order | Creation time is sensitive |
| It never leaves your infrastructure | It 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.