UUID v4 vs v7: We Benchmarked Generation, Sorting, and Index Locality
Real numbers from Node 20 on an M2 Pro: 24M v4/sec vs 2.7M v7/sec, why v7 is only 50% sorted in bursts, and a 35× index-locality gap. Code included.
"Use UUID v7 for primary keys" has become standard advice — including in our own UUID explainer. But advice is cheap, so we sat down and measured the three claims behind it: generation speed, sortability, and index locality. Some results matched the folklore. One genuinely surprised us.
Everything below ran on an Apple M2 Pro with Node 20.10. The full benchmark code is in this post, so you can rerun it and check our numbers.
The setup
Node has no built-in v7 yet, so we implemented it straight from RFC 9562: a 48-bit Unix millisecond timestamp, the fixed version and variant bits, and 74 random bits. To keep the comparison fair we used the same entropy-pooling trick the popular uuid npm package uses — without it, calling randomBytes(16) per ID makes v7 look 33× slower than it should:
import { randomFillSync } from 'node:crypto';
const POOL = Buffer.alloc(16 * 4096);
let poolPos = POOL.length;
function rand16() {
if (poolPos + 16 > POOL.length) {
randomFillSync(POOL);
poolPos = 0;
}
const out = POOL.subarray(poolPos, poolPos + 16);
poolPos += 16;
return out;
}
function uuidv7() {
const b = Buffer.from(rand16());
const ts = Date.now();
b[0] = (ts / 2 ** 40) & 0xff;
b[1] = (ts / 2 ** 32) & 0xff;
b[2] = (ts / 2 ** 24) & 0xff;
b[3] = (ts / 2 ** 16) & 0xff;
b[4] = (ts / 2 ** 8) & 0xff;
b[5] = ts & 0xff;
b[6] = (b[6] & 0x0f) | 0x70; // version 7
b[8] = (b[8] & 0x3f) | 0x80; // variant
const h = b.toString('hex');
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
}
v4 needs no library at all — crypto.randomUUID() is built into Node and every modern browser.
Round 1: generation throughput
Two million IDs each, after a 50k warmup:
| Generator | Throughput |
|---|---|
v4 — native crypto.randomUUID() | 24.3M ops/sec |
| v7 — pooled JS implementation (above) | 2.7M ops/sec |
v4 wins by ~9×, but read that gap carefully: it is the difference between V8-native code and a userland JavaScript function, not between the v4 and v7 layouts. Building a v7 costs one Date.now() and six byte writes more than a v4 — nothing that explains 9×. A native v7 would land close to native v4.
More importantly: both numbers are absurdly fast. If your service creates even 10,000 IDs per second — a busy system — you are using 0.4% of the slower generator's capacity. Generation speed should not factor into your choice at all. We measured it mostly so we could tell you to stop worrying about it.
Round 2: sortability — the result that surprised us
The v7 pitch is "IDs sort by creation time." We tested it directly: generate IDs, then count how many adjacent pairs are in ascending order.
| Scenario | Adjacent pairs in order |
|---|---|
| v4, generated in a burst | 50.1% (coin flip, as expected) |
| v7, generated in a burst | 50.1% |
| v7, paced at ≤1 per millisecond | 100.0% |
That middle row is the surprise. In a tight loop our machine generates thousands of v7s within the same millisecond, and inside one millisecond the timestamp prefix is identical — ordering falls to the random bits, which is a coin flip. Across milliseconds v7 sorts perfectly, which is why the paced run hits 100%.
The practical readings:
- For database indexes this is fine. Same-millisecond IDs land on the same neighborhood of the index either way; that is the locality you were buying.
- For strict ordering it is not fine. If your code assumes "generated later ⇒ sorts later" within a request or a batch, plain v7 breaks that assumption. RFC 9562 describes optional monotonic counter schemes for exactly this; some libraries implement them, many do not. Check yours before relying on it — or compare ULID implementations, many of which guarantee per-process monotonicity.
Round 3: index locality
The real argument for v7 is what happens inside a B-tree. We modeled it minimally: insert 20,000 IDs one at a time into a sorted array (binary-searching each position), and record how far from the tail each insert lands. An append lands at distance 0; a random insert lands anywhere.
| Generator | Avg insert distance from tail (20k rows) |
|---|---|
| v4 | 5,021 |
| v7 | 142 |
v4's average distance is about a quarter of the table — the uniform-random result, meaning every insert touches an effectively random region of the index. v7 lands ~35× closer to the hot end; its only disorder is the same-millisecond shuffling from Round 2.
To be clear about what this is: a sorted array is a model of index locality, not a real B-tree with pages, fill factors, and WAL. But the mechanism it isolates — random inserts touch cold regions, ordered inserts stay hot — is exactly what shows up in real databases: one PostgreSQL benchmark inserting 10 million rows measured v7 inserts roughly 35% faster than v4 with a 22% smaller index, and it is why our UUID guide recommends v7 keys for new insert-heavy tables.
What we'd actually choose
| Situation | Our pick |
|---|---|
| Primary keys, insert-heavy tables | v7 — the locality win is real |
| Session tokens, idempotency keys, filenames | v4 — order is irrelevant, zero dependencies |
| IDs that must not reveal creation time | v4 — v7's timestamp is readable by anyone who holds the ID |
| Strict monotonic ordering within a process | A library with RFC 9562 counters, or a monotonic ULID |
One honest caveat to close: benchmarks are specific. Different hardware, Node versions, or a native v7 implementation will move the throughput numbers (the sortability and locality results are properties of the formats and will reproduce anywhere). Rerun the code on your stack before quoting the exact figures — that is what it is in the post for.
Frequently asked questions
Is UUID v7 slower to generate than v4?
In our Node 20 benchmark, native crypto.randomUUID() (v4) produced 24.3M IDs/sec while a pooled JavaScript v7 implementation produced 2.7M IDs/sec. The gap comes from native code vs a JS library, not from the v7 layout itself — and both rates are orders of magnitude beyond what real applications generate.
Are UUID v7 values always in sorted order?
Only down to the millisecond. The 48-bit timestamp prefix orders IDs across milliseconds, but within the same millisecond the remaining bits are random. In our burst test only 50.1% of adjacent pairs were ordered — statistically identical to random. RFC 9562 describes optional monotonic counters implementations can add to fix this.
Does v7 actually help database indexes?
Yes. In our sorted-insert model each new v4 landed on average 5,021 positions from the tail of a 20,000-row index, while v7 landed 142 positions away — about 35× better locality. Time-ordered IDs keep inserts near the same B-tree pages — the same mechanism behind a PostgreSQL benchmark that measured v7 inserts roughly 35% faster than v4 at 10 million rows.
When should I still prefer UUID v4 over v7?
When IDs are not database keys (session tokens, idempotency keys, filenames), when you must not leak creation time — v7 embeds a millisecond timestamp readable by anyone — or when you depend on APIs that only ship v4, like the built-in crypto.randomUUID().