UUID v4 vs v7

Version 7 trades 48 bits of randomness for a timestamp. That trade buys index locality and costs you a publicly readable creation time.

Updated 2026-09-05

The specification, and which one to cite

RFC 9562 was published in May 2024 on the Standards Track and obsoletes RFC 4122. It is the document to cite, and several pages ranking for this query still cite 4122, which no longer describes the format they are talking about. Version 7 does not exist in RFC 4122 at all.

Two things changed that affect ordinary use. Versions 6, 7 and 8 were added, with 7 being the time-ordered layout most people now want. And the casing rule was relaxed: RFC 4122 mandated lowercase output, while 9562 permits upper, lower or mixed on output and requires case-insensitive comparison on input. Lowercase remains the sensible default.

Both versions are 128 bits rendered as 32 hexadecimal digits in five hyphenated groups. In both, six of those bits are fixed: four for the version and two for the variant. That is why every v4 UUID has a 4 as the thirteenth hex digit and a character from 8, 9, a or b as the seventeenth, and every v7 has a 7 in that same thirteenth position.

Version 4, from RFC 9562 section 5.4
BitsFieldContents
0-47random_a48 random bits
48-51verAlways 0100, rendered as the digit 4
52-63random_b12 random bits
64-65varAlways 10
66-127random_c62 random bits

Version 7, field by field

Version 7 spends the first 48 bits on a big-endian count of milliseconds since the Unix epoch. Forty-eight bits of milliseconds runs out in the year 10889, so this is not a problem anyone reading this will have. The remaining 74 bits are random, split across rand_a and rand_b around the version and variant fields.

Because the timestamp occupies the most significant bits and is big-endian, the lexicographic order of the rendered strings and the numeric order of the raw bytes both match creation order. That is the entire trick, and it is why v7 exists.

Within a single millisecond, ordering is arbitrary unless you do something about it. RFC 9562 section 6.2 gives three monotonicity methods. Method 1 dedicates a fixed-width counter inside rand_a, at least 12 bits and no more than 42, which increments for each UUID generated in the same millisecond and resets when the clock ticks. That is the method to implement for bulk generation, and it is what this site ships: fifty thousand consecutively generated values were strictly lexicographically increasing and all distinct.

Method 2, which increments a counter by one across the whole random field, has a consequence the RFC states directly: adjacent identifiers become trivially guessable. If your identifiers are public that is an enumeration vulnerability, not a performance optimisation.

Version 7, from RFC 9562 section 5.7
BitsFieldContents
0-47unix_ts_msBig-endian milliseconds since the Unix epoch
48-51verAlways 0111, rendered as the digit 7
52-63rand_a12 bits, random or a sub-millisecond counter
64-65varAlways 10
66-127rand_b62 random bits

What the B-tree actually does with each

The performance argument for v7 is usually stated as a slogan about locality. The mechanism is worth spelling out, because it explains where the benefit exists and where it does not.

A B-tree index stores keys in sorted order across fixed-size pages. When you insert a random key, it lands at an arbitrary point in that order, so an arbitrary leaf page has to be read, modified and written. With a large table, the page you need is usually not in the buffer pool, so every insert is a read followed by a write. The working set for inserts is the whole index rather than a small hot region. When a page fills, it splits down the middle, leaving two pages roughly half full, and since the next insert lands somewhere else entirely, those half-empty pages stay half empty. The index inflates and its cache efficiency falls.

A time-ordered key lands at the right-hand edge every time. The same leaf page is hot in the buffer pool for thousands of consecutive inserts. Splits at the right edge can be handled as an append rather than a fifty-fifty division, so pages fill completely. RFC 9562 section 6.11 puts the resulting difference at one order of magnitude or more, which matches the shape of what people measure.

The size of the win depends on the engine, and this is where most articles generalise incorrectly. In InnoDB the table is clustered on the primary key, so a random primary key scatters the rows themselves, not just an index. In PostgreSQL the table is a heap and the primary key is one index among several, so a random key hurts that index and leaves the heap append-only. And SQL Server documents that ordering for the uniqueidentifier type is not implemented by comparing bit patterns, which means a v7 UUID stored in that type does not deliver the locality benefit at all. The generic advice to always use v7 is wrong on at least one mainstream engine.

Two storage points that cost more than the version choice. Store UUIDs as 16 raw bytes, not as varchar(36): the text form more than doubles the index size and turns every comparison into a string comparison. And on a hash-partitioned distributed store such as DynamoDB, Cassandra or Spanner, the argument reverses entirely, because a monotonically increasing key concentrates every write on one partition. Randomness is the feature there.

What version 7 leaks

A v7 UUID publishes its own creation time to the millisecond. Two lines of code recover it, and no key or access is required, because the timestamp is not encrypted or obfuscated. It is just there, in the first twelve hex digits.

The concrete consequences land whenever those identifiers appear in a URL, an email, an API response or a support ticket. A competitor who signs up and reads the identifier in their own dashboard URL learns roughly when your account numbering started. Two customer identifiers reveal which account was created first and how far apart. An invoice identifier in a public link reveals when the invoice was raised, which may contradict what the invoice says. In a system where identifiers are visible to users, sampling a handful of them over time gives an outsider a fairly accurate signup rate, which is a metric most companies do not publish on purpose.

None of this is a break of the format. It is the format working as specified. The rule that follows is simple: v7 for internal primary keys where the creation time is not a secret, and v4 or a purpose-built random token for anything externally exposed that must be unguessable.

On unpredictability, the honest comparison is 122 random bits for v4 against 74 for v7. Both are above NIST's 64-bit floor, so a v7 UUID is not weak in the abstract. But 48 of its 128 bits are a value the attacker can often narrow to a small window, and the monotonic counter modes turn some of the remaining bits into a predictable sequence. Neither version was designed as a bearer token, and v7 is the worse of the two for that job.

A version 7 UUID generated for this article decoded to 2026-09-05T03:25:35.869Z. Paste any v7 value into our UUID inspector and it will do the same for you, which is the point: anyone can.

Collision arithmetic, done separately for each

Version 4 has 122 random bits, which is 5.317 x 10^36 possible values. Reaching a one-in-a-billion chance of a single collision takes about 103 trillion UUIDs, and the fifty percent birthday bound is around 2.7 x 10^18. Note that this is computed from 2^122, not 2^128. The commonly quoted figures based on 2^128 ignore the six fixed bits and are wrong, if only slightly.

Version 7 has 74 random bits, but they only need to be unique within one millisecond, because the timestamp separates everything else. Within a single millisecond a one percent collision chance arrives at roughly 1.95 x 10^10 values. No system generates twenty billion identifiers in a millisecond, and if you have a monotonic counter in rand_a you have removed the possibility entirely for the values that counter covers.

For both versions the practical risk is not the mathematics. It is a broken entropy source: a generator built on Math.random, or a fleet of container instances started from an image that had already seeded a userspace pool.

Where each version is natively supported

Version 4 is available everywhere. crypto.randomUUID in browsers and Node, uuid.uuid4 in Python, UUID.randomUUID in Java, Guid.NewGuid in .NET, gen_random_uuid in PostgreSQL 13 and later.

Version 7 has arrived in platforms over the last two years and no longer needs a library in several of them. PostgreSQL 18 ships a native uuidv7 function usable directly as a column default. .NET 9 added Guid.CreateVersion7. The google/uuid package added NewV7 in v1.6.0. Java still needs java-uuid-generator, Python still needs a library on 3.13 and earlier, and JavaScript has no native v7 at all, since crypto.randomUUID produces v4 and only v4.

If you implement it yourself, there is one bug worth knowing about because it appears in several widely copied snippets. JavaScript bitwise operators coerce their operands to signed 32-bit integers, and a millisecond timestamp is around 1.79 x 10^12. Writing ms >>> 40 to extract the high byte silently returns garbage. Use division by powers of two instead.

Doing this in your own code

Version 4
// format() renders the 16 bytes as lowercase 8-4-4-4-12 hex.
function uuidv4() {
  const b = crypto.getRandomValues(new Uint8Array(16));
  b[6] = (b[6] & 0x0f) | 0x40;   // version 4
  b[8] = (b[8] & 0x3f) | 0x80;   // variant 10
  return format(b);
}

// Or just use the built in, which is v4 and only ever v4.
crypto.randomUUID();

Reproduces the test vector in RFC 9562 appendix A.3 byte for byte when the same 16 bytes are supplied.

Version 7, monotonic
// RFC 9562 section 6.2 method 1: a dedicated counter in rand_a, so two
// UUIDs minted in the same millisecond still sort in creation order.
function makeUuidV7() {
  let lastMs = -1, seq = 0;
  return function uuidv7() {
    const ms = Date.now();
    if (ms === lastMs) {
      seq++;
      if (seq > 0xfff) { while (Date.now() === lastMs); return uuidv7(); }
    } else { lastMs = ms; seq = 0; }
    const b = crypto.getRandomValues(new Uint8Array(16));
    // Division, NOT >>>. Bitwise operators coerce to signed 32-bit and
    // Date.now() is around 1.79e12, so "ms >>> 40" returns garbage.
    // This is a live bug in several widely copied snippets.
    b[0] = (ms / 2 ** 40) & 0xff;  b[1] = (ms / 2 ** 32) & 0xff;
    b[2] = (ms / 2 ** 24) & 0xff;  b[3] = (ms / 2 ** 16) & 0xff;
    b[4] = (ms / 2 ** 8)  & 0xff;  b[5] =  ms            & 0xff;
    b[6] = 0x70 | (seq >>> 8);     // version 7 plus counter bits 11..8
    b[7] = seq & 0xff;             // counter bits 7..0
    b[8] = (b[8] & 0x3f) | 0x80;   // variant; all 62 rand_b bits stay random
    return format(b);
  };
}
Reading the timestamp back
// 48 bits fits inside a double's 53-bit integer range, so this is exact.
function uuidv7Timestamp(uuid) {
  return parseInt(uuid.slice(0, 8) + uuid.slice(9, 13), 16);
}

uuidv7Timestamp("017f22e2-79b0-7cc3-98c4-dc0c0c07398f");
// -> 1645557742000  ->  2022-02-22T19:22:22.000Z

// Anyone who has the UUID can run these two lines. That is the leak.
Native support
-- PostgreSQL 18
SELECT uuidv7();
CREATE TABLE orders (id uuid PRIMARY KEY DEFAULT uuidv7());

Also native: Guid.CreateVersion7() in .NET 9, and uuid.NewV7() in google/uuid from v1.5.0.

Questions

Should I migrate my existing v4 primary keys to v7?

Usually not on its own merits. The insert-path benefit applies to new rows, and a migration means rewriting every foreign key that references those values. The stronger case is to adopt v7 for new tables and high-insert tables, and to leave settled tables alone. Both versions are valid UUIDs and can coexist in the same column.

Can I sort v7 UUIDs as strings?

Yes. The timestamp is big-endian and occupies the most significant bits, and the hex rendering preserves that order, so lexicographic sorting of the canonical lowercase strings matches creation order. Mixed-case values break this, so normalise the case if you are sorting text.

Is a v7 UUID safe to use as a session token?

No. It has 74 unpredictable bits, of which none of the first 48 are unpredictable at all if the attacker can guess when the session began. A bearer token should be at least 128 bits from a CSPRNG with nothing derived from time or sequence.

Does v7 leak anything besides the timestamp?

Not by itself. Unlike version 1 it contains no node identifier and no MAC address. If you use section 6.2 method 2 monotonicity, which increments across the random field, adjacent identifiers become guessable, and the RFC says so explicitly. Method 1, with a dedicated counter in rand_a, does not have that problem.

How much faster is v7 as a primary key, really?

RFC 9562 section 6.11 says index locality can differ by an order of magnitude or more. The size of the effect depends on the engine, the size of the index relative to the buffer pool, and the insert rate. On a small table that fits in memory the difference is close to nothing, because every page is already cached. The benefit is real and it is not universal.

Try it

Related reading