UUID v4 vs v7: Which UUID Version Should You Use?
Drafted with AI assistance. Every command and code example was run and its output checked before publication. How guides are made
Use UUID v7 for database primary keys and anything you want sorted by creation time, and UUID v4 when the identifier should reveal nothing, including when it was made. Both are defined in RFC 9562 and are 128 bits long: v4 is 122 random bits, while v7 starts with a 48-bit Unix timestamp in milliseconds followed by 74 random bits, so new v7 values sort after older ones. That ordering keeps B-tree indexes compact; the price is that anyone holding a v7 UUID can read its creation time.
What UUID versions are there?
RFC 9562 (May 2024) replaced RFC 4122, kept versions 1 to 5, added 6, 7 and 8, and defined the Max UUID alongside the Nil UUID. Every UUID is written as 32 hexadecimal digits in groups of 8-4-4-4-12. The first digit of the third group is the version, and the first digit of the fourth group is 8, 9, a or b for the RFC variant.
| Version | Built from | Sortable by time | Use today |
|---|---|---|---|
| 1 | 60-bit timestamp (100 ns since 1582) + clock sequence + node ID, traditionally the MAC address | No, timestamp bytes are out of order | Legacy only |
| 2 | DCE Security; not specified by RFC 9562 | – | Avoid |
| 3 | MD5 hash of a namespace UUID and a name | No | Prefer v5 |
| 4 | 122 random bits | No | General-purpose IDs |
| 5 | SHA-1 hash of a namespace UUID and a name | No | Same input must give the same ID |
| 6 | v1 fields reordered so the timestamp comes first | Yes | Migrating from v1 |
| 7 | 48-bit Unix time in ms + 74 random (or counter) bits | Yes | Database keys, event IDs |
| 8 | 122 bits laid out however the implementer chooses | Depends | Custom or experimental layouts |
| Nil | All zeros: 00000000-0000-0000-0000-000000000000 | – | "No value" sentinel |
| Max | All ones: ffffffff-ffff-ffff-ffff-ffffffffffff | – | "Highest value" sentinel |
RFC 9562 says implementations SHOULD use v7 instead of v1 and v6 where possible. Version 5 is useful when the same name must always map to the same UUID, for example uuid.uuid5(uuid.NAMESPACE_DNS, "example.com") in Python is always cfbff0d1-9375-5685-968c-48ce8b15ae17. The RFC warns against name-based UUIDs as primary keys, because the "name" they are derived from tends to change.
How random is a v4 UUID, and can two collide?
A v4 UUID fixes 6 bits (4 for the version, 2 for the variant) and fills the other 122 from a random source, so there are 2122 ≈ 5.3 × 1036 possible values. By the birthday approximation, the chance that any two of n random v4 UUIDs are equal is about n2 / 2123:
| UUIDs generated | Probability of at least one collision |
|---|---|
| 106 (a million) | 9.4 × 10−26 |
| 109 (a billion) | 9.4 × 10−20 |
| 1012 (a trillion) | 9.4 × 10−14 |
| 1.03 × 1014 | One in a billion |
| 2.7 × 1018 | One in two |
These figures assume a cryptographically secure random number generator (CSPRNG), which RFC 9562 says implementations SHOULD use. In practice, duplicate UUIDs come from weak or badly seeded generators (for instance a forked process that copies its parent's random state), from Math.random()-based libraries, or from copying values, not from the arithmetic.
How is a UUID v7 laid out?
Reading the 128 bits from left to right:
- 48 bits
unix_ts_ms: milliseconds since 1970-01-01T00:00:00Z, the same number as JavaScript'sDate.now(). This is the first 12 hex digits. 48 bits last until the year 10889. - 4 bits version: always
7. - 12 bits
rand_a: random, or extra clock precision, or a counter. - 2 bits variant:
10in binary, so the 17th hex digit is 8, 9, a or b. - 62 bits
rand_b: random, or partly a counter.
Because the timestamp sits in the most significant bits, comparing two v7 UUIDs byte by byte, or as lowercase strings, orders them by creation time to the millisecond. Within one millisecond, order depends on the generator. RFC 9562 section 6.2 describes optional ways to stay monotonic: a counter in the leftmost random bits, or sub-millisecond clock bits in rand_a. Python 3.14's uuid7() uses a 42-bit counter; PostgreSQL 18's uuidv7() uses sub-millisecond timestamp bits. The site's UUID Generator fills all 74 bits with random data and sorts each batch it generates, so a list from one click is in ascending order.
You can read the time back out of any v7 UUID:
function uuidv7Time(uuid) {
return new Date(parseInt(uuid.replace(/-/g, "").slice(0, 12), 16));
}
uuidv7Time("019b76da-a800-7a3e-9c41-5d2e8f0b7a61").toISOString();
// '2026-01-01T00:00:00.000Z'
Is v7 better than v4 for database keys?
For B-tree indexes, which back primary keys in PostgreSQL, MySQL InnoDB and most other relational databases, usually yes. The reason is where each new key lands. A B-tree keeps keys sorted in fixed-size pages. Random v4 keys are spread uniformly across the key space, so consecutive inserts hit different leaf pages all over the index. Each insert needs its target page in memory, pages fill up and split at arbitrary points, and over time the index has many partly empty pages. Once the index is larger than the cache, many inserts must first read a page from disk.
Time-ordered v7 keys behave much like an auto-increment column: each new key is larger than almost all existing ones, so inserts go to the rightmost leaf page. The pages being written stay in cache, and filled pages are left behind as the index grows. RFC 9562 gives "poor database-index locality" of non-time-ordered UUIDs as one of its motivations and notes that the effect on B-trees "can be dramatic". How much it matters for you depends on table size, memory and write rate, so measure with your own workload rather than relying on someone else's benchmark.
v4 is still fine for small tables, for non-key columns, and for databases that do not use a B-tree on the key. And v7 gives no ordering guarantee across machines whose clocks differ: it sorts by each generator's idea of the time.
Does a UUID v7 leak information?
Yes: the creation time to the millisecond. Anyone who sees a v7 ID in a URL can tell when the account, order or document was created, and can compare the IDs of two records. If that is sensitive, use v4 for public identifiers, or keep a v7 primary key internal and expose a separate v4 or random token. RFC 9562 says that if UUIDs are needed for any security operation, v4 SHOULD be used.
Neither version is a secret. RFC 9562 states that UUIDs MUST NOT be used as security capabilities (identifiers whose mere possession grants access). For password-reset links, session IDs or API keys, use a dedicated random token and check permissions on the server. Version 1 is worse on privacy: its node field traditionally holds the generating machine's MAC address.
How do I generate a UUID v4 or v7 in code?
JavaScript
crypto.randomUUID() returns a lowercase v4 string. In browsers it is available only in secure contexts (HTTPS or localhost); in Node.js it is a global.
crypto.randomUUID(); // e.g. '63d58e27-12ca-490a-aa08-9d83de08ebd5'
For v7, the Node.js documentation lists crypto.randomUUIDv7() as added in v26.1.0; on older versions, the uuid package on npm exports v7() (checked with version 14). Or write it yourself in a few lines:
function uuidv7() {
const bytes = crypto.getRandomValues(new Uint8Array(16));
let ms = Date.now();
for (let i = 5; i >= 0; i--) { bytes[i] = ms % 256; ms = Math.floor(ms / 256); }
bytes[6] = (bytes[6] & 0x0f) | 0x70; // version 7
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
const hex = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
Python
The standard uuid module has had uuid4() for a long time. uuid6(), uuid7(), uuid8(), NIL and MAX were added in Python 3.14; on earlier versions you need a third-party package.
import uuid
uuid.uuid4() # UUID('0c416fd0-b664-4f8d-87b2-3b26142e5389')
u = uuid.uuid7() # Python 3.14+
u.version # 7
u.time # milliseconds since the epoch, for v7
str(u) # 36-character lowercase string
u.bytes # 16 raw bytes
PostgreSQL
gen_random_uuid() (v4) is built in from PostgreSQL 13; before that it came from the pgcrypto extension. PostgreSQL 17 added uuid_extract_version() and uuid_extract_timestamp(), and PostgreSQL 18 added uuidv7() and uuidv4() (UUID functions).
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(), -- PostgreSQL 18+
name text NOT NULL
);
SELECT uuid_extract_timestamp('019b76da-a800-7a3e-9c41-5d2e8f0b7a61');
-- 2026-01-01 00:00:00+00 (with TimeZone set to UTC)
Should I store UUIDs as binary or text?
Binary, where you can. RFC 9562 section 6.13 points out that the text form takes 288 bits to represent a 128-bit value and says UUIDs SHOULD be stored as the underlying binary value in databases. Smaller keys mean smaller indexes, and every foreign key that references them shrinks too.
- PostgreSQL: use the native
uuidtype, which is 16 bytes and accepts upper case, braces or no hyphens on input while always outputting lowercase with hyphens. - MySQL 8: there is no UUID column type; use
BINARY(16)withUUID_TO_BIN()andBIN_TO_UUID(). Do not pass the swap flag (UUID_TO_BIN(id, 1)) for v7: it moves bytes around to make MySQL's own v1 values sortable, and it scrambles the order of a v7. Note that MySQL'sUUID()function generates version 1. - Text columns work everywhere but cost 36 bytes or more per value. If you must, store one case only (lowercase), because text comparison is often case-sensitive and the same UUID in two cases will not match or sort together.
How do I validate a UUID?
This regular expression accepts the hyphenated form of RFC 9562 versions 1 to 8, in either case:
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
UUID_RE.test("550e8400-e29b-41d4-a716-446655440000"); // true
UUID_RE.test("550e8400e29b41d4a716446655440000"); // false (no hyphens)
UUID_RE.test("00000000-0000-0000-0000-000000000000"); // false (nil: check separately)
To accept only v4 or only v7, replace [1-8] with 4 or 7. Test variations in the Regex Tester, or paste a value into the validator on the UUID Generator page, which reports the version and recognises the nil and max UUIDs. A parser is more forgiving than a regex: Python's uuid.UUID() also accepts braces, a urn:uuid: prefix and the 32-digit form, as does PostgreSQL's uuid type.
Checklist
- Primary keys in a B-tree index: v7. Public IDs whose creation time must stay hidden: v4.
- Deterministic IDs from a name: v5, not as a primary key.
- Generate with a cryptographically secure source (
crypto.randomUUID(),crypto.getRandomValues, Python'suuid), neverMath.random(). - Store as a native
uuidtype or 16 bytes; if text, lowercase only. - Never use a UUID as a password, reset token or API key.
- Remember v7 order is only as good as the clocks that generate it.
Frequently asked questions
Can I convert a v4 UUID to v7?
No. A v4 contains no timestamp, so there is nothing to convert. To move a table to v7 keys, generate new v7 values (from the row's creation time if you have one) and keep the old v4 in a separate column for existing references.
Are UUID v7 values guaranteed to be unique?
Not guaranteed, but collisions need two values from the same millisecond with the same 74 random bits. Even at a million UUIDs in one millisecond, the chance of a clash in that millisecond is about 3 × 10−11. Generators that add a monotonic counter avoid repeats within one process.
Is a GUID the same as a UUID?
Yes, GUID is Microsoft's name for the same 128-bit format. RFC 9562 notes one caveat: Microsoft COM GUIDs use little-endian order when saving the first fields as bytes, so the raw bytes can differ from the RFC's big-endian order even when the text is identical.
How do I get the creation time from a UUID v7?
Take the first 12 hex digits, parse them as a base-16 integer, and treat it as milliseconds since the epoch. In PostgreSQL 17 or later, uuid_extract_timestamp() does it for you; the Unix Timestamp Converter turns the number into a date, and the Unix timestamps guide explains the units.
Should I use UUIDs or auto-increment integers?
Auto-increment integers are smaller and simple when a single database assigns all IDs. UUIDs let clients, services and offline devices create IDs without coordinating, and they do not reveal how many rows you have. If you choose UUIDs for keys, v7 gives you most of the index behaviour of a sequence.