Skip to content

UUID versions explained: v4, v7 and when to use each

By · UUID & Random Generators · 5 min read · Updated

UUIDs let any machine create an identifier that will not collide with identifiers created anywhere else, without coordinating with a central database. That makes them popular for primary keys, request IDs, file names and idempotency keys. But "UUID" covers several different versions, and the choice between them affects database performance, privacy and sorting. This guide explains how UUIDs are built and when to use v4, v7 or something else.

Anatomy of a UUID

A UUID is a 128-bit number, written as 32 hexadecimal digits in five groups separated by hyphens:

f47ac10b-58cc-4372-a567-0e02b2c3d479
              ^    ^
              |    variant (8, 9, a or b)
              version

The first digit of the third group is the version. The first digit of the fourth group encodes the variant, and for standard UUIDs it is always 8, 9, a or b. Everything else depends on the version. The format is defined in RFC 9562, published in 2024, which replaced the older RFC 4122 and added versions 6, 7 and 8.

The versions you will meet

Version 1: time and MAC address

v1 combines a 60-bit timestamp with the network card's MAC address. It is unique, but it leaks which machine created the ID and when, and its byte layout does not sort by time because the low bits of the timestamp come first. It is rarely a good choice today.

Version 3 and 5: name-based

These hash a namespace and a name, with MD5 for v3 and SHA-1 for v5. The same inputs always produce the same UUID. That is useful when you need a stable ID for something that already has a unique name, such as a URL, without storing a mapping table.

Version 4: random

v4 is 122 random bits plus the version and variant markers. It reveals nothing about where or when it was created, and is supported everywhere: crypto.randomUUID() in browsers and Node.js, uuid.uuid4() in Python, UUID.randomUUID() in Java, gen_random_uuid() in PostgreSQL. It is the default most developers reach for, and you can create them in bulk with the UUID Generator.

Version 7: time-ordered

v7 starts with a 48-bit Unix timestamp in milliseconds, followed by random bits. UUIDs created later sort after UUIDs created earlier, while remaining globally unique without coordination. The first 12 hex digits of a v7 UUID are the creation time, which you can read with a timestamp converter after converting the hex to decimal.

Can two random UUIDs collide?

In theory yes, in practice no. With 122 random bits, you would need to generate roughly 2.7 quintillion v4 UUIDs before reaching a 50% chance of a single collision. A more realistic risk is a bad random number generator. UUIDs must come from a cryptographically secure source; implementations that use Math.random() or a predictably seeded generator have caused real collisions. Use your platform's built-in UUID function.

Why v4 primary keys can hurt databases

Most relational databases store tables or indexes as B-trees ordered by key. When keys increase steadily, like an auto-increment integer, new rows are appended to the rightmost page of the index, which stays hot in memory. Random v4 UUIDs land on a random page every time. On large tables this means:

  • Many more pages must be read from disk and kept in memory during inserts.
  • Pages split in the middle and end up partly empty, so the index grows larger than necessary.
  • Write-ahead logs and replication traffic grow, because more pages are modified.

In MySQL with InnoDB, where the table itself is clustered by primary key, the effect is particularly strong. PostgreSQL suffers less because tables are heaps, but its primary key index still sees the random writes.

Time-ordered UUID v7 fixes this. New keys are always near the end of the index, so inserts behave much like an auto-increment column while keeping the benefits of UUIDs.

ULID and other alternatives

Before v7 was standardised, the ULID format solved the same problem: a 48-bit millisecond timestamp plus 80 random bits, written as 26 characters of Crockford Base32, such as 01J9ZQK3T8M4XG2V7N5R6WBCDE. ULIDs sort correctly as strings and are shorter than UUID text. If your system already uses ULIDs, there is no urgent reason to switch; for new systems, UUID v7 has the advantage of native database types and wide library support.

Other options include Twitter's Snowflake IDs and similar 64-bit schemes, which fit in a bigint but need a unique worker ID per generator, and therefore some coordination.

Generating them in code

Use the built-in generator where your platform has one: crypto.randomUUID() in browsers and Node.js, UUID.randomUUID() in Java, and gen_random_uuid() in PostgreSQL 13+ all produce v4. For v7, PostgreSQL 18 added uuidv7() and Python 3.14 added uuid.uuid7(); elsewhere, use a maintained library such as uuid on npm. The format is simple enough that a dependency-free version fits in a few lines, which also shows exactly where the bits go:

function uuidv7() {
  const b = crypto.getRandomValues(new Uint8Array(16));
  let ms = Date.now();
  for (let i = 5; i >= 0; i--) { b[i] = ms % 256; ms = Math.floor(ms / 256); }  // 48-bit timestamp
  b[6] = (b[6] & 0x0f) | 0x70;   // version 7
  b[8] = (b[8] & 0x3f) | 0x80;   // RFC 9562 variant
  const h = [...b].map((x) => x.toString(16).padStart(2, '0')).join('');
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
}

uuidv7()   // '01a12234-f83d-7a29-89f7-71986ade563c'
// the first 12 hex digits are the creation time in ms:
new Date(parseInt('01a12234f83d', 16)).toISOString()   // '2026-10-09T19:47:39.197Z'

Two IDs generated a few milliseconds apart compare in creation order as plain strings, which is the property that keeps database indexes happy. IDs created within the same millisecond are ordered randomly; libraries that need strict ordering use some of the random bits as a counter, which RFC 9562 allows.

Storing UUIDs efficiently

  • Use the native type where one exists: uuid in PostgreSQL, uniqueidentifier in SQL Server, or BINARY(16) in MySQL. A CHAR(36) column uses more than twice the space and slows every index and join.
  • Normalise the text format at your API boundary: lowercase with hyphens is the convention. The same UUID in uppercase or without hyphens is the same value, but a string comparison will not think so.
  • Be careful with SQL Server, which sorts uniqueidentifier values using an unusual byte order, so v7 UUIDs do not sort by time there without extra work.

Things UUIDs are not

A UUID is an identifier, not a secret. Even random v4 UUIDs are not designed as security tokens, and v1 and v7 reveal creation time. For password reset links, session IDs and API keys, generate at least 128 bits from a secure random source specifically for that purpose and store only a hash of it. And because v7 exposes a timestamp, avoid it for public identifiers where revealing when a record was created is a privacy concern, such as user account IDs in some products.

Recommendation

SituationChoice
Database primary keys in new systemsUUID v7
Public IDs where creation time should stay privateUUID v4
Request, trace and idempotency keysv4 or v7, both fine
Stable ID derived from an existing nameUUID v5
Secrets and tokensNot a UUID; use dedicated random tokens