Randomly

Identifiers

UUID v4 vs v7: random IDs versus time-ordered keys

Bipul Kumar 8 min
UUID v4 vs v7: random IDs versus time-ordered keys
On this page 0%

I used to treat UUID version as a checkbox. Pick v4, ship it, never think about the index again. Then a table with a few million rows started to feel sluggish on inserts, and the reason was hiding in the first bytes of the primary key. v4 is random. v7 is time-ordered. They both look like the same hyphenated hex string. They do not behave the same in a B-tree.

This is the version of that argument I wish I had when I was still pasting IDs out of a REPL. I will walk through the bit layout, when v4 is still the right call, when v7 earns the extra timestamp, and how to generate both in the browser with the UUID generator.

Diagram of a 128-bit UUID split into fields, with the version nibble highlighted
Same 128 bits. The version nibble is the part that actually changes the story.

What a UUID actually is

A UUID is 128 bits, usually printed as 32 hex digits and four hyphens: 8-4-4-4-12. RFC 9562 is the current document. It folds in the old RFC 4122 rules and adds v6, v7, and v8. You do not need to memorize the RFC. You do need to know that the string is not “just random hex.” Byte 6 carries a version nibble. Byte 8 carries variant bits. If those bits are wrong, libraries will still print the string, and then some other library will reject it.

I still see people concatenate Math.random() fragments and call it a UUID. That is a random string. It is not a UUID. Collision math assumes the version and variant bits are set, and that the remaining bits come from a decent CSPRNG. Our generator uses crypto.getRandomValues for the random parts. That is the same Web Crypto surface browsers ship for a reason.

UUID v4, the random one

Version 4 fills almost everything with random bits. After you stamp version 4 and the RFC variant, you have 122 bits of entropy. The usual cocktail-napkin line is that you would need to generate billions of IDs per second for centuries before a collision becomes likely. I do not worry about v4 collisions in application code. I worry about people treating “unlikely” as “I can skip the unique constraint.” Put the unique index on. The UUID is not a substitute for a constraint.

v4 is still my default when the ID must not leak time. Session tokens, password-reset IDs, anything that might show up in a URL where a sequential value would let someone guess the next one. Time-ordered IDs are not a secret. If the first 48 bits are a millisecond timestamp, a reader can see when the row was created, plus or minus clock skew.

v4 is also what most older docs mean when they say “UUID.” Java UUID.randomUUID(), Python uuid.uuid4(), Postgres gen_random_uuid() in current versions: random, version 4. If you drop v7 into a system that assumed v4, nothing explodes immediately. Sorting and locality change. That is the part that bites later.

Side-by-side comparison of shuffled v4 UUIDs versus time-ordered v7 UUIDs
v4 scatters inserts across the index. v7 appends near the hot end of the B-tree.

UUID v7, the time-ordered one

Version 7 puts a 48-bit Unix timestamp in milliseconds in the high bits, then random bits after that. New IDs sort roughly in creation order. If two IDs are minted in the same millisecond, the random tail breaks the tie. That is enough for most databases that care about insert locality.

Why care? Because random primary keys make clustered indexes jump around. On InnoDB, the primary key is the table. Random inserts mean page splits, fatter indexes, more I/O. People spent a decade arguing about UUIDs versus bigints for this reason. v7 is the boring compromise: still 128 bits, still unique without a central allocator, but friendly to B-trees.

I use v7 for rows that are written often and read by recency: events, messages, uploads, anything you might ORDER BY id and mean “newest.” I do not use v7 when the ID is a secret, or when a client could mint IDs offline with a lying clock. Clock rollback is a real footgun. RFC 9562 talks about monotonic counters inside the same millisecond. Our browser tool is not a distributed clock. It stamps Date.now() and random bytes. That is correct for tests and for single-page generation. It is not a replacement for a server that coordinates time.

v4 versus v7 in practice

If I am writing a fixture file, I want v4. The timestamp in a v7 would freeze to whatever afternoon I generated the dump, and then every test run would look like it happened on that day. Random IDs keep fixtures from pretending they know the clock.

If I am choosing a primary key for a new Postgres table in 2026, I look at v7 first. Postgres 18 even grew more UUID helpers; the ecosystem caught up. I still add a created_at column. Sorting on the UUID is a convenience, not a substitute for an explicit timestamp you can index and filter.

GUIDs in Windows and .NET are the same 128 bits with different braces and uppercase. If you need that dress-up, use the GUID generator. It is v4 underneath, printed the way registry keys expect.

How to read the version

Take a UUID. Look at the first hex digit of the third group. That is the version. xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx. M is 4 for v4, 7 for v7. N should be 8, 9, a, or b for the RFC variant. If you generate ten IDs and M is all over the place, you are not talking to a real UUID library.

I pasted a “UUID” from a tutorial once that used version 0 because someone had masked the bits wrong. Validators were kind. The database was not.

Simple UUID generator UI with v4 and v7 toggles and a generated identifier
Toggle v4 or v7, generate a batch, copy. The bit-stamping happens locally.

Generate them without leaving the tab

Open the UUID generator. Pick v4 or v7. Set a count if you want a handful for a seed file. Generate. Copy. Nothing is posted to our servers. If you also need opaque API keys rather than UUIDs, the random string generator is the better shape: no version nibble, just entropy.

For hashing an ID into a digest (please do not “hash a UUID to make it more unique”), see SHA-256. That is a different job.

Mistakes I still see

Storing UUIDs as 36-character strings when the database has a 16-byte UUID type. You pay for the hyphens in every index. Use the native type if you have it.

Generating v7 in the browser and then trusting it as a server-side created-at. The user’s clock can be wrong. If time matters, set it on the server.

Assuming v7 is sortable across processes without thinking about clock skew. Two machines, two NTP moods, two slightly overlapping timelines. Fine for “roughly recent.” Bad for a financial ledger that needs a total order.

Using UUIDs as security tokens without checking entropy. v4 is fine. A truncated UUID is not. If you need a token, generate a token.

A short decision rule

Need an ID that must not hint at when it was made? v4. Need an ID that will be a primary key on a write-heavy table? v7. Need Microsoft braces? GUID formatter. Need something a human will type? None of these. Use a short token and accept that it is a different trade.

I keep both versions in the generator because I still use both in the same week. The string looks the same in a screenshot. The index does not care about screenshots.

Storage notes I learned the hard way

In Postgres, uuid is 16 bytes. text with hyphens is 36. If you copy IDs into JSON logs, you will store the hyphenated form anyway. That is fine. The mistake is using varchar(36) as the primary key type “because it looks like the docs.” You pay for it on every join.

MySQL people still argue about BINARY(16) versus CHAR(36). If you go binary, remember to convert at the edges. I have broken more admin UIs by returning raw bytes than by using the slower text column. Pick one representation at the API boundary and stick to it.

SQLite will happily store whatever string you give it. Cloudflare D1 is SQLite. Our CMS post IDs are text UUIDs. For a table that gets a few hundred writes a day, locality does not matter. For a table that gets a few hundred writes a second, it does. Be honest about which one you have.

One more habit: when I paste a UUID into Slack, I wrap it in backticks. Hyphens plus markdown equals a broken link and a very confused teammate.

Frequently asked questions

Can v4 and v7 collide with each other?

They share a 128-bit space but the version nibble differs, so a well-formed v4 will not equal a well-formed v7. Do not mix homemade strings into that assumption.

Is v7 standardized?

Yes. RFC 9562. Libraries caught up slower than the draft, which is why you still see blog posts treating v7 as experimental. It is not experimental anymore.

Should I migrate existing v4 primary keys?

Usually no. Rewriting keys is a gift you give your future incidents. Use v7 on new tables. Leave old ones alone unless you have a measured page-split problem and a migration window you trust.

Does Randomly store the UUIDs I generate?

No. The page stamps them locally. See privacy for what we do collect (analytics on page views, not your IDs).