Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why how

Why UUIDv7 is quietly replacing autoincrement IDs

Autoincrement IDs can't be minted client-side and leak how many rows you have. Random UUIDs trash your index. UUIDv7 is the boring fix almost nobody noticed shipping.

Data intermediate Apr 29, 2026

Why it exists

For most of the last twenty years, “what should the primary key be?” had two boring answers and an argument between them.

Engineers picked one and lived with the trade-off. UUIDv7 is the version that finally gets to stop picking. It keeps everything that made v4 nice — client-generated, no coordination, collision-free in practice — and puts a timestamp at the front, so the IDs sort in roughly the order they were created. That one change makes them play nicely with B-trees again.

It’s specified in RFC 9562 (May 2024), which replaced the old UUID RFC 4122 and added v6, v7, and v8. v7 is the one people actually adopted.

Why it matters now

The trade-off used to be tolerable because services were small and inserts were rare relative to reads. Two things changed:

Postgres 18 (released September 2025) shipped a built-in uuidv7() function. Prisma added UUIDv7 schema support in 2024. MySQL and SQLite don’t ship native v7 generators yet — their built-in UUID() functions predate v7 — so for now you get v7 there via libraries or manual generation. The trajectory is clear enough that “use UUIDv7” is becoming the unremarkable default for new schemas, which is exactly when it’s worth understanding why.

The short answer

UUIDv7 = 48-bit Unix-millisecond timestamp + 74 random bits (with version & variant tags)

A v7 UUID is just a UUID-shaped wrapper around “what time is it” and “some randomness.” Because the timestamp sits at the most-significant end, two v7s generated a millisecond apart sort in the right order; two generated in the same millisecond fall back to randomness for tie-breaking. You get v4-style independence and B-tree-friendly ordering at the same time.

How it works

The 128-bit layout, roughly (the version and variant nibbles take a few bits out of the random fields):

| 48-bit unix_ts_ms | ver=7 | 12 bits rand_a | var | 62 bits rand_b |

Two properties fall out of that layout:

  1. Lexicographic order ≈ creation order. Because the timestamp is the high-order bytes, comparing two v7s as raw bytes (or as strings, since the canonical hex form preserves order) gives you “older first” almost for free. New rows insert at the right edge of the B-tree, which is the cheap case: hot pages stay in cache, splits happen at the end, and the index doesn’t fragment.
  2. No coordination needed. Each generator just reads the clock and rolls up to 74 random bits. (The spec lets implementations spend some of those bits on sub-ms timestamp resolution or a monotonic counter; whatever is left is random.) For a fully random v7, two services on two continents producing one each in the same millisecond collide with probability ~1 in 2^74 — for any workload that fits on Earth, you can treat that as zero. Implementations that trade randomness for monotonicity have a smaller pool, but it’s still cosmically large.

Why v4 hurts your database

To see why ordering matters, watch a B-tree index take inserts. The leaf pages are stored in primary-key order on disk. With an autoincrement key, every new row goes at the end — one hot page, always in memory, always the target. With v4 UUIDs, each new row’s key is a random number between 0 and 2^128, so it lands in a random leaf page. That page probably isn’t in cache. The database faults it in, mutates it, marks it dirty, eventually writes it back. Multiply by every insert. You’ve turned a sequential write workload into a random one, which on both spinning disks and SSDs is a category change in cost.

The classic symptom: a service that benchmarks fine on a fresh database gets mysteriously slower as the table grows past whatever fits in the buffer pool. That’s the point where “random page reads on every insert” stops being free.

v7 puts insertions back at the right edge. You get almost the same one-hot-page behavior as autoincrement — randomness in the low bits means inserts spread across the few most recent leaf pages rather than landing on a single one — but that’s worlds closer to sequential than v4’s scatter, and your buffer pool will feel the difference.

Why v7 over a plain timestamp + random tail

You could roll your own “timestamp prefix + random suffix” ID and skip the RFC. People did, for years — Twitter’s Snowflake, ULID, KSUID, all variations on the theme. v7 is interesting because:

Where it gets fuzzy

The mental model worth keeping: a v7 is a v4 with the high bits replaced by “now.” That single change is doing all the work.

Going deeper