UUID v4 vs v7 vs ULID: What the Public Benchmarks Actually Show
16 July, 2026 Backend
I sat down intending to run yet another UUID benchmark, and stopped halfway through writing the docker-compose file. The internet does not need benchmark number forty-seven — the public ones already agree with each other. What I couldn't find anywhere was someone putting them side by side and explaining why one measures a 35% improvement, another a 50% one, and a third a throughput collapse to less than a third. Those numbers are not contradictory. They are the same curve, sampled at different points. This article reconciles them.
The Mechanism, in One Paragraph
InnoDB stores table rows physically ordered by primary key; PostgreSQL keeps the primary key in a B-tree index. Either way, a random key (UUID v4) means every insert lands at a random position in that structure — a random page must be read, modified, split when full. A time-ordered key (UUID v7, ULID) means every insert lands at the rightmost edge — the same few hot pages, always in cache, splitting predictably. The byte-level structure of these formats is covered in UUID vs ULID; this article is only about what that difference costs.
Four Benchmarks, One Story
| Source | Database | Scale | Headline result |
|---|---|---|---|
| Percona (Yves Trudeau) | MySQL/InnoDB | Table grown past buffer pool | Insert rate collapses from CPU-bound to IO-bound the moment the index exceeds RAM |
| 2ndQuadrant (Tomas Vondra) | PostgreSQL | Medium scale (index > shared buffers) | Random-UUID throughput drops below 30% of small-scale rate; 20+ GB of WAL, mostly full-page images |
| PostgreSQL 16, 10M rows | PostgreSQL 16 | 10M rows, NVMe, 16 GB RAM | v7 inserts ~35% faster; v7 index ~22% smaller (619 MB vs 793 MB) |
| Shopify production | MySQL (payments) | High-throughput production system | 50% decrease in INSERT statement duration after switching idempotency keys from UUID v4 to ULID |
Percona: where the cliff is
Trudeau's MySQL benchmark (128 MB buffer pool, 500 IOPS cap — deliberately modest, like a small cloud instance) shows the shape of the problem rather than a single number: insertion with random UUID keys is CPU-bound while the table fits in the buffer pool, then degrades rapidly to IO-bound the moment it doesn't, because every insert now needs a disk read of a random page. With pseudo-ordered keys the hot edge of the B-tree stays cached — his arithmetic: a 1 TB table needs only ~16 MB of buffer pool to sustain ordered inserts. The same post records a customer whose UUID choices accounted for 70% of a ~1 TB schema across primary keys, secondary indexes and foreign keys — the storage cost compounds, because every secondary index and every referencing table carries a copy of the key.
Vondra: the WAL amplification nobody budgets for
The 2ndQuadrant benchmark adds the PostgreSQL-specific penalty: after each checkpoint, the first modification to any page writes a full-page image into the WAL. Random keys touch nearly every index page between checkpoints, so the random-UUID runs produced over 20 GB of WAL, the vast majority full-page images, while throughput fell to under 30% of the in-memory rate. Sequential-prefix UUIDs held a ~99% buffer cache hit ratio on the same workload. If you pay for replication bandwidth or PITR storage, this is the line item that surprises you.
The 10M-row run: what a healthy machine shows
The PostgreSQL 16 benchmark is the moderate case: 10M rows on NVMe with plenty of RAM. Even with the whole working set cacheable, v7 came out ~35% faster on inserts with a 22% smaller index — fragmentation costs you before the cliff, too, just less dramatically. Point lookups and range scans were also measurably faster against the denser v7 index.
Shopify: the production data point
Benchmarks are one thing; Shopify's payments team reports the change in anger — switching idempotency keys from UUID v4 to ULID halved INSERT duration in one of their high-throughput MySQL systems. Worth noting what it says about scale: this is a table with heavy concurrent writes and a working set no buffer pool fully holds, which is exactly where the theory predicts the biggest win.
Why the Numbers Differ So Much
Line the results up against one variable — the ratio of index size to available cache — and the spread stops being confusing:
- Index fits comfortably in RAM (the 10M-row run): random keys cost you page-split overhead and a fatter, half-empty index. Penalty: tens of percent.
- Index roughly at cache size (Shopify): every random insert has a meaningful chance of missing cache. Penalty: ~2x.
- Index far larger than cache (Percona's capped VM, Vondra's medium scale): nearly every insert is a random disk read plus WAL amplification. Penalty: 3x and worsening as the table grows.
This is why "should I care about v4 vs v7?" has no single answer. The honest answer is: look at your index size, your RAM, and your write rate. A 200k-row table will never notice. A table adding millions of rows a month is walking toward the cliff whether or not it has arrived.
What I'd Actually Do
- New schema: UUID v7 as the default primary key. Same 16 bytes, same column types, same wire format as v4 — the time-ordering is free. Generate a few with the UUID v7 generator and you can see the shared timestamp prefix within a millisecond. When the ecosystem is yours to choose and shorter strings matter, ULID delivers the same index behaviour in 26 characters.
- Existing v4 schema, no measured pain: stay put. Every source above agrees the penalty is a function of scale; migrating a key format is expensive and the win below the cache threshold is modest.
- Store 16 bytes, not 36 characters.
binary(16)in MySQL, nativeuuidin PostgreSQL. Percona's 70%-of-a-terabyte customer is whatchar(36)keys look like after a few years of foreign keys. - One caveat before you switch: v7 and ULID expose creation time to anyone who sees the ID. For internal keys, irrelevant; for public-facing identifiers, decide deliberately - the trade-off is laid out in UUID versions explained.
The benchmarks are unanimous on direction and honest about magnitude only in context. Take the number from the row of the table above that looks like your system — not the most dramatic one.