UUIDv4 vs UUIDv7: why ordered IDs matter.

Reach for a random v4 UUID as a database primary key and, at scale, your inserts slow down — random keys scatter writes across the whole index. v7, standardized in RFC 9562, prefixes a millisecond timestamp so IDs sort by creation time. Here's the bit layout byte by byte, why it matters for how databases actually store rows, and when each version is the right call.

You're designing a new table. The primary key needs to be unique, unguessable, and generatable on the client without a round-trip to the database. So you reach for a UUID — specifically uuidv4, the random one, because that's the version every library hands you by default. It works. Ship it.

Two years and forty million rows later, inserts are slow, the index is twice the size it ought to be, and a database consultant is pointing at your primary key. The UUID was the right idea. The version was wrong. This is the case for UUIDv7 — and the surprisingly large difference a timestamp prefix makes.

What a UUID actually is.

A UUID is 128 bits — sixteen bytes — usually written as 32 hexadecimal digits in five hyphenated groups:

f47ac10b-58cc-4372-a567-0e02b2c3d479
└──────┘ └──┘ └──┘ └──┘ └──────────┘
   8      4    4    4        12      hex digits

Not all 128 bits are free. Two small fields are reserved. The version (4 bits) records how the UUID was generated; the variant (2 bits) identifies the layout standard. In the example above, the 4 opening the third group is the version nibble, and the a opening the fourth group encodes the variant. That leaves 122 bits to carry actual information — and what you put in those 122 bits is the entire difference between v4 and v7.

v4 fills all 122 bits with cryptographically random data. v7 spends the first 48 of them on a Unix timestamp in milliseconds, then fills the rest with randomness. Same size, same practical uniqueness — but v7's bytes are no longer in random order.

Why random v4 keys hurt your database.

Databases don't store rows in the order you insert them. They store them in a B-tree ordered by the primary key — this is literally how InnoDB's clustered index and SQL Server's clustered index lay rows out on disk. When you insert a row, the engine finds the right leaf page for that key and writes the row there.

With a sequential key — an auto-increment integer, say — every insert lands on the same page: the last one. That page stays hot in memory, fills up, a new page is appended, and on you go. Writes are cheap and the index stays compact.

With a random v4 key, every insert lands on a random page somewhere in the tree. At forty million rows the index is far too large to sit in memory, so each insert likely touches a page that isn't cached — a read to fetch it, a modification, a write to flush it. Worse, inserting into the middle of a full page forces a page split: the engine cuts the page in half to make room, leaving both halves half-empty. Do that millions of times and the index bloats to roughly twice its necessary size, with cache-hostile random I/O on every write.

The short version: random primary keys turn cheap sequential writes into expensive random ones, and fragment the index as they go. The pain is invisible at a thousand rows and severe at tens of millions.

What v7 changes.

Because v7 puts a millisecond timestamp in its high-order bits — the bits that dominate sort order — UUIDs generated close together in time are numerically close together. Insert them and they land on the same hot page, the way an auto-increment integer would, while still being globally unique and generatable on the client.

You keep everything that made UUIDs attractive: no central sequence to coordinate, no insert round-trip, no small integer IDs quietly leaking your row count to the world. And you get back the write performance and index compactness of a sequential key. Numbers vary by engine and workload, but on a large InnoDB table the gap between random and time-ordered keys is routinely measured in multiples, not percentages — both on insert throughput and on final index size.

A bonus: v7 IDs sort chronologically on their own. ORDER BY id is effectively ORDER BY created_at for free, and a range of IDs corresponds to a range of time — handy for keyset pagination and for sharding by creation window.

The bit layout, byte by byte.

Here is where the 128 bits go in each version. "random" is cryptographically random data; "ver" and "var" are the fixed version and variant fields.

Bit rangeUUIDv4UUIDv7
0–47 (first 48)randomUnix time, ms
48–51version 0100version 0111
52–63randomrandom
64–65variant 10variant 10
66–127randomrandom

The only structural difference is those first 48 bits. v4 spends them on entropy; v7 spends them on a timestamp. Forty-eight bits of milliseconds reaches past the year 10000, so it will not run out. The remaining 74 random bits are far more than enough to avoid collisions — even generating millions of v7 IDs in the same millisecond, the odds of two colliding are negligible, and RFC 9562 describes optional monotonic counters for systems that need a hard ordering guarantee within a single millisecond.

One consequence is worth saying out loud: because the timestamp sits right there in the high bits, a v7 UUID reveals when it was created. Anyone holding the ID can read its creation time to the millisecond. That's a feature for debugging and a leak for privacy — which is exactly the hinge for choosing between the two.

When v4 is still the right choice.

v7 is the better default for primary keys, but v4 is still correct in several cases:

When the ID is exposed and its creation time is sensitive. Password-reset tokens, share links, API keys, invite codes — anything user-facing where you don't want the holder to know when it was minted. A v7 token broadcasts its own age. Use v4 (or a purpose-built random token) here.

When you specifically don't want time-adjacency. v7 IDs created in sequence are numerically near each other; the 74 random bits make actually guessing a valid neighbour infeasible, but if your threat model dislikes even the structure, v4's full randomness is cleaner.

When the value isn't a database key at all. A request-correlation ID in a log line, a random filename, a one-off idempotency key — cases with no index to fragment and no ordering to gain. v4 is perfectly fine, and there's no reason to migrate code that already works.

Choosing and migrating.

A simple rule covers most situations:

Use v7 for database primary keys and internal identifiers where insert performance and natural time-ordering help and the creation time isn't a secret. Use v4 for externally-exposed tokens, secrets, and any ID whose creation time you'd rather not disclose.

Support is now broad. PostgreSQL 18 ships a native uuidv7() function, and where the database doesn't generate them, every major language ecosystem has a library — the standard library in newer runtimes, a small dependency otherwise. RFC 9562, published in May 2024, made v7 an official standard rather than a draft, so you're no longer betting on a moving target.

Migrating an existing table is the harder part: you generally don't rewrite historical keys (the old random ones still work) — you switch new rows to v7 so the index stops fragmenting going forward, and let the table heal over time. If you're greenfield, just start with v7 and skip the lesson the rest of us learned at forty million rows.

Takeaways.

The thing to remember: v4 and v7 are the same size and equally unique in practice. The only difference is that v7 puts a timestamp in the bits that decide sort order, which turns random index writes into sequential ones and makes IDs time-ordered for free. Default to v7 for keys; keep v4 for anything where the ID is exposed and its creation time should stay private.

UUIDs were always a good idea — globally unique, no coordination, client-generatable. v7 just stops you paying for that convenience with index fragmentation. It's the rare upgrade that costs nothing and pays back at exactly the scale where it starts to matter.

Generate v4 and v7 IDs in your browser.

The IDs tool generates both UUIDv4 and UUIDv7 — with the timestamp prefix visible — in bulk and copy-ready. Everything runs in your browser; nothing is sent to a server, which matters when the IDs you generate are about to become real keys in your system.

Open the IDs tool

Made with love by a very serious person pretending not to be. Tooly McToolface is a workshop of free, client-side web tools. If you liked this byte-by-byte breakdown, reading a JSON Web Token field by field is the same kind of "what's actually in these bytes" piece, and ISO 8601 vs Unix epoch digs into the timestamps that v7 quietly embeds.