Developer

UUID v4 vs v7: Which One to Use, and What v7 Costs You

v7 for database keys, v4 when the creation time is nobody else’s business. The bits behind that rule, and the two places v7 quietly does nothing.

Use v7 when the UUID is going to be a database primary key. Use v4 when the identifier will be seen by people who have no business knowing when the row was created. That is the entire decision, and it comes down to one structural difference: a v7 UUID starts with a 48-bit Unix timestamp in milliseconds, so values generated later sort after values generated earlier. A v4 UUID is 122 random bits and nothing else, so it sorts nowhere in particular and reveals nothing at all.

What is actually different in the bits?

Both are 128 bits, written as 32 hex digits in the 8-4-4-4-12 pattern that comes to 36 characters with the hyphens. Both spend 4 bits on a version nibble and 2 on a variant marker. The rest is where they diverge.

v4v7
Layout122 random bits48-bit millisecond clock, then 74 random bits
Sorts into creation orderNoYes, across milliseconds
Reveals when it was madeNoYes, to the millisecond
GuessableNoNo
Reach for it whenThe value is public and the timing is notThe value is a primary key

The version lives in the thirteenth hex digit, the one immediately after the second hyphen: a 4 or a 7. If you have an identifier in front of you and no idea which kind it is, the inspector on the UUID generator reads that nibble and, for a v7, decodes the leading bytes back into a date and time.

That leading value is an ordinary count of milliseconds since 1 January 1970 UTC — the same number a Unix timestamp holds, stored as big-endian hex instead of written out in decimal. Nothing clever is happening there. It is a clock reading glued to the front of a random number.

Why does v7 index better?

Databases keep primary keys in a sorted B-tree. A v4 key lands at a random point in that tree. On a small table this costs nothing, because the whole index is in memory. On a large one the database fetches the target page from disk, and when that page is full it splits it in two, leaving both half-empty. A few million inserts later the index is bigger than it needs to be, thinly spread, and constantly being read back from storage.

A v7 key appends. Every value is bigger than the ones generated in earlier milliseconds, so inserts land on the rightmost page of the tree — the one already in memory because you just wrote to it. The pages fill completely instead of splitting down the middle, and the set of pages being touched stays small no matter how big the table gets.

The honest caveat: this only starts to matter once the index stops fitting in RAM. On a small table you will measure nothing, and anyone quoting a dramatic speedup without naming their row count and their hardware is quoting a benchmark built to produce one. The reason to default to v7 anyway is that the table you have in three years is the one that will care, and changing a primary key type after the fact is genuinely painful.

What does a v7 UUID reveal?

The timestamp is not encrypted or obfuscated. Anyone holding a v7 UUID can read the millisecond it was made, and anyone holding two can read the gap between them. If the identifier shows up in a URL, an invoice or an API response, you have published that.

Sometimes that is harmless. Sometimes it is a competitor counting your order IDs over a week to estimate your sales rate, or a user working out that an account marked "member since 2019" was created last Tuesday. Decide which you are before putting v7 in a public field.

What v7 does not do is make identifiers guessable. The remaining 74 random bits are far too many to enumerate, so nobody is walking your database by incrementing a UUID. The leak is the timestamp, not the sequence.

Neither version is a secret. UUIDs are treated as identifiers everywhere downstream, so they end up in server logs, analytics events, Referer headers and screenshots pasted into chat. If you need a password reset token or an unguessable share link, use a password generator built for secrets and store a hash of it, rather than reusing an ID half your stack is writing to disk in plain text.

Where does v7 not help?

SQL Server, if you use the native type

SQL Server's uniqueidentifier does not compare bytes left to right. It treats the last six bytes as the most significant, then works backwards through the earlier groups. A v7 UUID puts its timestamp in the first six bytes — exactly the ones SQL Server checks last. So v7 values scatter through a clustered index almost as badly as v4 ones, and the benefit evaporates. Storing them as binary(16) instead, or using a COMB-style GUID that puts the increasing part at the end, is the usual workaround.

Inside a single millisecond

Two v7 UUIDs made in the same millisecond have identical timestamps, so their relative order is decided by the random bits — which is to say, it is arbitrary. RFC 9562 allows part of the random field to be spent on monotonicity instead, either a counter or extra clock precision, and some implementations do it: PostgreSQL's built-in uuidv7() puts sub-millisecond clock precision in those bits. The generator here does not; it fills them with randomness, which is compliant but means a batch of a thousand made in one millisecond has no internal order. Across milliseconds the ordering is exact. If you rely on UUID order as a tiebreaker in a hot write path, check what your library actually does.

And v7 ordering is only as good as the clocks producing it. Two servers a hundred milliseconds apart interleave their IDs, and a clock that steps backwards produces values that sort before ones written earlier. Good enough for index locality, not a source of truth about sequence.

Is v4 still a reasonable default?

Yes, for anything that is not a high-volume primary key. It has been the sane choice for twenty years and has not stopped working. Collisions are not the worry for either version. The birthday bound puts a 50% chance of a single v4 collision at roughly 2.7 quintillion identifiers; for v7, where only values sharing a millisecond can clash at all, it takes about 160 billion of them inside that same millisecond.

What does go wrong is a weak random source. Older code often assembled UUIDs by hand out of Math.random, which is fast, not cryptographic, and never meant for this. The result passes every validator, because the version nibble still reads 4 and the bits look like any other UUID from the outside. No inspector can catch it — not the one on this site either, since it reads bits and has nothing else to go on. The only way to know is to open the code and see whether it calls the platform's crypto API.

What about ULID, v1 and v6?

ULID predates v7 and solves the same problem: 48 bits of millisecond timestamp plus 80 random bits, written as 26 characters of Crockford base32 instead of hex. It is shorter and case-insensitive, but it is not a UUID, so database types, validators and ORMs will not recognise it without help. v7 is the standardised version of the same idea.

v1 is the original time-based UUID: the clock is split into three chunks in the wrong order for sorting, and the last six bytes are a node identifier that was historically the machine's MAC address. v6 is v1 with those fields rearranged so they sort, and exists mainly to let systems already holding v1 values migrate. v3 and v5 are a different tool again — they run a namespace and a name through a hash function, so the same input always produces the same UUID. None of them is the right answer for new work that needs fresh unique values.

How should you store a UUID?

As 16 bytes, not as 36 characters. A CHAR(36) column stores the hyphens and the hex spelling rather than the value itself, and every index built on it carries the same dead weight — which undoes a good part of what you switched to v7 for.

PostgreSQL has a native uuid type. Version 18, released in September 2025, added a built-in uuidv7(); uuid_extract_timestamp() arrived earlier, in version 17, and now reads v7 values as well as v1. MySQL has no UUID type at all, so BINARY(16) with UUID_TO_BIN at the edges is the usual arrangement. Leave that function's swap flag off for v7: it swaps the first and third groups of hex digits, which is what a v1 UUID needs to sort and exactly what wrecks a v7 one. MySQL's own manual says the swap only benefits version 1 values.

Normalise the text form to lowercase with hyphens before it reaches the database, whichever type you land on. That is the canonical representation, and a table holding both spellings of the same identifier is a bug hunt nobody enjoys.

If you just need identifiers rather than a decision, the UUID generator makes both kinds a thousand at a time and will take an existing one apart to tell you what version it is and, for a v7, when it was made. It runs entirely in your browser and takes its randomness from the platform's cryptographic generator, not from Math.random.

One thing people try next is shrinking the 36-character text form by encoding the raw 16 bytes instead, which gets you down to 22 characters. What Base64 is, and when you actually need it covers what that encoding does to your data and why the URL-safe variant exists — worth reading before you put the short form in a route.

Frequently asked questions

Should I use UUID v4 or v7?

Use v7 for database primary keys and anything else that gets indexed at volume, because its leading timestamp makes inserts append to the end of the index instead of scattering through it. Use v4 when the identifier is public and the creation time should stay private. For low-volume internal IDs either works fine.

Is UUID v7 safe to expose in a URL?

It is unguessable, but it is not private. The first 48 bits are the creation time in milliseconds, readable by anyone holding the value, so a public v7 ID publishes when the record was made. The remaining 74 random bits mean nobody can enumerate your records, so the only real leak is the timestamp.

Are v7 UUIDs more likely to collide than v4?

On paper yes, in practice no. A v4 UUID has 122 random bits against v7’s 74, but two v7 values can only collide if they were generated in the same millisecond, and reaching a 50% chance of that takes roughly 160 billion of them inside that one millisecond. With either version the realistic risk is a weak random source, not the bit count.

Does UUID v7 work in SQL Server?

It generates and stores fine, but the performance benefit does not survive the native uniqueidentifier type. SQL Server compares those values starting from the last six bytes, and v7 puts its timestamp in the first six, so the values still land randomly in a clustered index. Store them as binary(16) or use a sequential GUID scheme designed for SQL Server instead.

What is the difference between UUID v7 and ULID?

They encode the same idea: a 48-bit millisecond timestamp followed by random bits. ULID uses 80 random bits and a 26-character base32 text form, while v7 uses 74 random bits and the standard 36-character UUID form. v7 is part of RFC 9562, so databases, validators and libraries recognise it natively; ULID usually needs extra handling.

How do I tell which UUID version I have?

Look at the thirteenth hex digit, which is the first character after the second hyphen. It holds the version number, so a 4 there means a random v4 and a 7 means a time-ordered v7. The character after the third hyphen is the variant, which is why so many UUIDs have an 8, 9, a or b in that spot.

Last updated September 19, 2026