A UUID v4 is almost entirely random, while a UUID v7 puts a millisecond timestamp in its leading bits and fills the rest with randomness — which makes v7 values sortable by creation time and far friendlier as database keys, at the cost of revealing when they were made. Both are 128-bit identifiers that look identical at a glance, but that structural difference changes how they behave inside an index. Understanding the layout is the key to knowing which to reach for.
How is a UUID structured?
Every UUID is 128 bits — 16 bytes — usually written as 32 hexadecimal digits in the familiar hyphenated 8-4-4-4-12 shape:
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
Two small fields are reserved regardless of version. Four bits encode the version (the M position above), and two bits encode the variant (the top bits of the N position). For a version 4 UUID the version nibble is literally 4; for version 7 it is 7. That leaves 122 bits for the actual payload, and what fills those 122 bits is the entire difference between the versions.
| Field | Bits | Purpose |
|---|---|---|
| Version | 4 | Which UUID version (4 or 7) |
| Variant | 2 | Layout family (the common variant) |
| Payload | 122 | Random and/or timestamp data |
What is a UUID v4 and where does it fall short?
A v4 UUID is random noise: 122 of its 128 bits come from a random or pseudo-random source, with only the version and variant bits fixed. That makes v4 wonderfully simple. You need no coordination, no central authority, and no shared state — any machine can mint one independently and the collision probability is negligible because the space is astronomically large. For opaque tokens, correlation IDs, and any identifier that must reveal nothing, v4 is ideal.
The weakness shows up only when you use v4 as a primary key in a database that stores rows physically ordered by that key. Because consecutive v4 values are unrelated random numbers, a stream of new keys scatters across the whole index. Two rows created a second apart can land at opposite ends of the B-tree. That scattering has costs we will get to; first, look at how v7 fixes it.
// Two v4 values created moments apart — no relationship, no order
f47ac10b-58cc-4372-a567-0e02b2c3d479
9e1a7c33-2b90-4d51-8f6e-1c4477aa02be
What changed in UUID v7?
v7 replaces the leading 48 bits of random data with a Unix timestamp in milliseconds, then fills the remaining bits with randomness. The layout is deliberately front-loaded with time:
| Bits | Contents |
|---|---|
| First 48 | Unix time in milliseconds (big-endian) |
| Next 4 | Version (7) |
| Next 12 | Random (or an optional sub-millisecond counter) |
| Next 2 | Variant |
| Final 62 | Random |
Because the timestamp occupies the most significant bits, the ordinary sort order of the raw bytes matches the order in which the values were created. A v7 made now sorts after one made a minute ago and before one made a minute from now. The trailing random bits still guarantee uniqueness — even two UUIDs generated in the same millisecond differ in their 74 random bits — while the leading timestamp gives them order. The 48-bit millisecond field is large enough to represent dates for thousands of years, so overflow is not a practical concern.
There is one subtlety worth knowing. Within a single millisecond, two v7 values share the same timestamp prefix and are distinguished only by their random tails — which means their relative order is not guaranteed to reflect the exact sub-millisecond moment they were created. For most applications that does not matter; the ordering is still correct at millisecond granularity, which is finer than most workloads need. But when strict monotonic ordering under a very high generation rate is required, an implementation may use some of the bits immediately after the timestamp as a counter that increments for each UUID minted in the same millisecond, guaranteeing that values created later in that window sort later. This is optional and implementation-specific; the core guarantee remains that the leading 48 bits are the millisecond timestamp and the remainder provides uniqueness.
// Two v7 values; note the shared, increasing leading segment (the timestamp)
018f3a2c-7b1e-7c44-9a01-6f2b9d4e10aa
018f3a2c-7c93-7f52-8b77-2ad5c1e9f003
// same-ish millisecond prefix, different random tails
Why is a time-ordered UUID better as a database key?
Because inserting time-ordered keys keeps new rows clustered at the end of the index instead of scattering them, which is much easier on a B-tree. Most relational databases keep an index (and often the table itself) ordered by the primary key using a B-tree structure. When you insert keys that are always larger than the last, every new row goes to the rightmost edge — the “hot” end — so the database keeps touching the same few pages, they stay cached, and the tree grows tidily.
Random keys behave the opposite way. Each insert targets an arbitrary position, so the database must read and modify pages all over the index. Two problems follow:
- Page splits. Inserting into the middle of a full page forces it to split into two, fragmenting the index and increasing its size over time.
- Poor cache locality. Because inserts hit random pages, far more of the index must stay in memory to avoid disk reads, and recently created rows are not physically near each other.
v7 sidesteps both. New rows append near the end, page splits become rare, the working set of hot pages stays small, and rows created around the same time sit close together — which also speeds up range scans like “the most recent N records.” You get the coordination-free, globally-unique nature of a UUID with much of the insert-friendliness of an auto-incrementing integer. If you want to see the version nibble and the byte layout for yourself, a client-side UUID generator lets you mint v4 and v7 values and compare their structure without any data leaving your browser.
What is the tradeoff with UUID v7?
The same timestamp that makes v7 sortable also makes it leak: anyone who holds a v7 value can read roughly when it was created. Those leading 48 bits are just the creation time in milliseconds, and they are not encrypted or obscured — they are plainly recoverable. That has real consequences depending on where the identifier travels:
- A v7 exposed in a URL, an API response, or a public record discloses the creation time of whatever it names — a user account, an order, a document.
- Sequential v7 values can reveal rate information: how many records were created in a window, or the relative timing of two events.
- For a public, guessable-adjacency identifier, time-ordering can make enumeration or correlation slightly easier to reason about.
v4 leaks none of this precisely because it is random; that is its whole value proposition. So the choice is not “v7 is the new v4.” It is a genuine tradeoff:
| Need | Prefer |
|---|---|
| Internal primary key, insert performance matters | v7 |
| Identifier exposed publicly, timing must stay private | v4 |
| Opaque token that should reveal nothing | v4 |
| Recent-records range scans are common | v7 |
A common pattern is to use v7 for the database key that lives inside your system, where its ordering pays off, and to expose a separate opaque handle externally when you do not want to advertise creation times.
Which should you use?
Reach for v7 when the UUID is a primary key or is otherwise stored in an ordered index and you value clustered inserts, fewer page splits, and fast recent-first scans — and when it is acceptable that the value encodes its creation time. Reach for v4 when the identifier is public-facing or security-sensitive and must not disclose timing, or when you simply want a coordination-free random token and index behavior is not a concern. Both are 128 bits, both carry the version and variant fields, and both are unique for practical purposes; the difference is entirely in those leading bits — random in v4, a millisecond clock in v7 — and everything else about their behavior flows from that one design decision.