UUID v4 vs v7: Which One to Generate

UUID v4 has been the default choice for generating unique identifiers for years, but v7 was standardised specifically to fix a performance problem that only shows up once an application has real data volume. This post compares the two layouts and explains when the difference actually matters. The generator under /dev-tools can produce either version if you want to see the structure directly.
Layout of a UUID v4
A UUID is 128 bits, usually written as 32 hex digits split into groups by hyphens, like xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx. Version 4 sets almost all of those bits from a random number generator, with only a small number of fixed bits reserved to mark the version (the 4) and the variant (the first digit of the fourth group, constrained to 8, 9, a or b).
This means a v4 UUID carries no information about when or where it was created. Two v4 UUIDs generated a second apart or a year apart look equally random and give no hint of ordering, which is exactly the point when the identifier should reveal nothing about its origin.
Layout of a UUID v7
Version 7 replaces the first 48 bits of the value with a millisecond-precision Unix timestamp, leaving the remaining bits random, again with the version and variant markers fixed in place. The result is that UUIDs generated later always sort after ones generated earlier when compared as plain strings or byte sequences, because the timestamp occupies the most significant bits.
The random portion after the timestamp is still large enough to make collisions between two IDs generated in the same millisecond effectively negligible for any realistic generation rate, so v7 does not meaningfully trade away uniqueness to gain ordering.
Randomness and collision maths in plain terms
A v4 UUID has 122 random bits, since 6 bits are fixed for version and variant. The often-quoted figure is that you would need to generate roughly a billion UUIDs per second for about 85 years before the probability of a single collision reached 50 percent, which is why v4 collisions are treated as a non-issue in practice.
A v7 UUID has fewer random bits, 74 rather than 122, because part of the space is now used for the timestamp. That is still an enormous number, 74 bits is over 18 sextillion possible values, so within any single millisecond window the chance of two independently generated v7 UUIDs colliding remains vanishingly small for any workload a normal application will see.
Index locality and B-tree page splits
Most relational databases store a primary key index as a B-tree, which keeps entries sorted. When new keys arrive in random order, as v4 UUIDs do, each insert lands in a random position across the tree rather than at the end, which forces the database to touch pages scattered throughout the index rather than the most recently used ones. As the table grows, more of those pages get evicted from cache between inserts, causing extra disk reads and page splits that fragment the index further.
Sequential keys, whether an auto-incrementing integer or a v7 UUID, insert at the tail of the index almost every time, since new values are always numerically or lexically larger than existing ones. That keeps writes localised to a small, cache-friendly region of the index, which is the main reason v7 was created: to get the uniqueness and decentralised generation of a UUID without the index fragmentation cost of pure randomness.
Sortability
Because the timestamp sits in the most significant bits, sorting a set of v7 UUIDs as plain strings produces the same order as sorting by creation time, which is a genuinely useful property beyond index performance. You can tell, just by comparing two v7 values, which one was created first, without needing a separate created_at column, though keeping one for clarity is still good practice.
V4 UUIDs have no such property. Sorting them as strings produces an order with no relationship to creation time at all, so any application that needs chronological ordering of v4-keyed records has to rely entirely on a separate timestamp column.
Timestamp leakage as a privacy consideration
The same property that makes v7 useful for indexing also means it leaks information: anyone who can see a v7 UUID can extract the exact millisecond it was created, down to reading the first 48 bits directly. For most internal identifiers this is harmless, but for identifiers exposed in public URLs, such as an order number or a document ID, it reveals precisely when the record was created, which can enable enumeration attacks that estimate volume over time or expose the timing of an otherwise private event.
If creation time is sensitive for a particular identifier, v4 remains the safer choice, or a v7 ID can be used internally for the database while a separate, unrelated public identifier is exposed to users.
Comparison with ULID
ULID is an older, non-standardised format that solves the same problem as v7: a 48-bit timestamp followed by 80 bits of randomness, usually encoded in Crockford base32 rather than hex, which makes it shorter and case-insensitive. Conceptually ULID and UUID v7 are close relatives, and the choice between them mostly comes down to whether you need to stay within the formal UUID standard, which many database columns, ORMs and validation libraries expect, or whether you are free to use a distinct format. Since v7 was standardised, most new projects that would previously have chosen ULID now default to v7 for the sake of broader tooling compatibility.
Decision guide
Choose v7 as the default for primary keys in a relational database where insert performance and index locality matter as the table grows, and where creation-time ordering is a nice side benefit rather than a concern. Choose v4 when the identifier is exposed publicly and its creation timestamp must not be inferable, or when you are working with existing systems and libraries that specifically expect random, non-sequential identifiers and you do not want to introduce a behavioural change.
Both versions are 128-bit values in the same textual format, so switching between them later is a data migration decision rather than a structural one, and a generator under /dev-tools makes it straightforward to produce either version while testing how each behaves in your own schema.
Worked example: reading a v7 UUID
Take a v7 UUID like 018f3a2e-7c40-7abc-8def-1234567890ab. The first 12 hex characters, 018f3a2e7c40, are the 48-bit millisecond timestamp; converting that hex value to decimal and treating it as milliseconds since the Unix epoch gives the exact creation time. The next character, 7, is the fixed version marker, and the character after that, 8, is the variant marker, both of which are always present regardless of the specific random values elsewhere in the UUID.
Everything after the version and variant markers, the remaining roughly 74 bits, is random and carries no meaning beyond ensuring uniqueness within that millisecond. This is why two v7 UUIDs generated in the same request often share the same first 12 characters but differ everywhere else.
Edge cases and migration considerations
Systems clock skew or a system clock that jumps backward, for example after an NTP correction, can produce a v7 UUID that is technically out of order relative to ones generated just before the jump. This does not break uniqueness, since the random bits still make a collision astronomically unlikely, but it does mean sort order is not a cryptographic guarantee, just a strong practical property based on system time behaving normally.
Migrating an existing table from v4 primary keys to v7 is not something you can do by simply changing the generator going forward, since existing v4 rows keep their original random keys; new rows would sort after old ones inconsistently, defeating the point of switching. Most migrations either accept mixed key types permanently, add a separate sequential column purely for indexing, or perform a full data migration that reassigns keys, which is a significant undertaking for a table with foreign key references elsewhere in the schema.
- A backward clock jump does not break uniqueness, only the ordering guarantee for that window
- Switching a generator to v7 does not reorder existing v4 rows already in the table
- Foreign key references complicate any full migration that reassigns existing primary keys
- Some database drivers store UUIDs as fixed 16-byte binary rather than a 36-character string; check how your schema stores them before assuming string comparison behaviour applies
FAQ
Is UUID v7 an official standard? Yes, it was formally added alongside v6 and v8 in RFC 9562, which updated the original UUID specification and gave time-ordered UUIDs an official, interoperable definition rather than leaving them to ad hoc formats like ULID.
Does v7 replace v4 entirely? No. v7 is better for database primary keys where insert locality matters, but v4 remains the right choice whenever an identifier must not reveal its creation time, or when compatibility with existing systems expecting pure randomness matters more than index performance.
Can I generate a v7 UUID without a library? Yes, conceptually it is just the current millisecond timestamp in hex followed by random bits with the version and variant markers set, though using a maintained library avoids subtle mistakes in bit placement that are easy to get wrong when implementing it from scratch.
Questions about the tools in this guide
Short answers about the hubs this article touches, each linking straight to the tool.