BytePane

UUID v4 vs v7: Format, Ordering and When to Use Each

Identifiers6 min read

Quick answer

Both are 128-bit identifiers written as 36 characters (8-4-4-4-12 hex). UUIDv4 is 122 random bits plus fixed version and variant bits, so consecutive IDs have no relationship. UUIDv7 starts with a 48-bit Unix millisecond timestamp, then version, then 74 random (or counter) bits, so IDs sort by creation time when compared as bytes or strings. RFC 9562 §5.7says implementations “SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible”. Use v7 for database keys and anything you will index or paginate by; use v4 when the creation time must not leak or you only need an opaque random ID.

Bit layout from RFC 9562

BitsUUIDv4 (§5.4)UUIDv7 (§5.7)
0–47 (48)random_aunix_ts_ms, Unix epoch timestamp in milliseconds, big-endian
48–51 (4)ver = 0100 (4)ver = 0111 (7)
52–63 (12)random_brand_a, random, or sub-millisecond / counter data
64–65 (2)var = 10var = 10
66–127 (62)random_crand_b, random, or counter data
Random bits12274 (12 + 62) when no counter is used
Sortable by timeNoYes, RFC 9562 §6.11: designed to “sort as opaque raw bytes”
Reveals creation timeNoYes (to the millisecond)
Index localityInserts scatter across the indexInserts append at the end

In the text form, the version is the first hex digit of the third group and the variant is the first hex digit of the fourth group (8, 9, a or b for the 10 variant). A regex for v4 therefore looks like ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; for v7 replace the 4 with 7.

Node.js: v4 is built in, v7 is 12 lines

crypto.randomUUID() returns a v4. The v7 generator below fills 16 random bytes, overwrites the first six with the millisecond timestamp, then forces the version nibble to 7 and the two variant bits to 10. The test generates three IDs two milliseconds apart and confirms they sort in generation order:

import { randomUUID, getRandomValues } from 'node:crypto';

console.log('v4:', randomUUID());

function uuidv7() {
  const bytes = new Uint8Array(16);
  getRandomValues(bytes);
  const ts = BigInt(Date.now());                          // 48-bit unix_ts_ms
  for (let i = 0; i < 6; i++) bytes[i] = Number((ts >> BigInt(8 * (5 - i))) & 0xffn);
  bytes[6] = (bytes[6] & 0x0f) | 0x70;                    // version 7 in the high nibble
  bytes[8] = (bytes[8] & 0x3f) | 0x80;                    // variant 10xxxxxx
  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)}`;
}

const ids = [];
for (let i = 0; i < 3; i++) { ids.push(uuidv7()); await new Promise(r => setTimeout(r, 2)); }
console.log(ids.join('\n'));
console.log('version nibble:', ids.map(u => u[14]).join(','), ' variant char:', ids.map(u => u[19]).join(','));
console.log('sorted as strings === generation order:', JSON.stringify([...ids].sort()) === JSON.stringify(ids));
console.log('timestamp of first:', new Date(parseInt(ids[0].replace('-', '').slice(0, 12), 16)).toISOString());

// $ node uuid.mjs
// v4: 7cf1cf06-ad98-4d5b-a29e-9c4fe362d082
// 01a0af3a-9bf0-747f-9a38-075ed05afea4
// 01a0af3a-9bf2-79fa-93a8-da0a2327282d
// 01a0af3a-9bf4-7660-a5b6-f6cba519cfdd
// version nibble: 7,7,7  variant char: 9,9,a
// sorted as strings === generation order: true
// timestamp of first: 2026-09-17T11:57:28.944Z

The three v7 values share the prefix 01a0af3a-9bf because they were created within the same few milliseconds; the v4 shares nothing with them. This minimal generator does not guarantee ordering for IDs created inside the same millisecond, RFC 9562 §6.2 describes counter and sub-millisecond techniques for that, and production libraries implement one of them.

Python: uuid4() everywhere, uuid7() from 3.14

import uuid

u = uuid.uuid4()
print("v4:", u, "version", u.version, "variant", u.variant)
print("has uuid7:", hasattr(uuid, "uuid7"))

# $ python3 uuid_test.py   (Python 3.10)
# v4: 1af54d9d-9885-4b25-bd49-47ee8051db63 version 4 variant specified in RFC 4122
# has uuid7: False

The Python documentation adds uuid.uuid7() in version 3.14: it generates “a time-based UUID according to RFC 9562, §5.7” and, for portability across platforms lacking sub-millisecond precision, embeds a 48-bit timestamp and uses a 42-bit counter to guarantee monotonicity within a millisecond. uuid6() and uuid8() arrive in the same release. On 3.13 and earlier, hasattr(uuid, "uuid7") is False and you need a third-party package or a port of the Node function above.

When to use which

SituationPickWhy
Primary key in a B-tree-indexed table with many insertsv7New keys are appended in time order instead of splitting pages at random positions
Cursor pagination or “newest first” ordering without a separate timestamp columnv7Byte order equals creation order at millisecond resolution
Public IDs where creation time is sensitive (invoices, user sign-ups)v4v7 exposes the timestamp to anyone holding the ID
Replacing v1 or v6 time-based IDsv7RFC 9562: “SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6”
Session IDs, password-reset tokens, secretsNeitherUUIDs are identifiers; use a dedicated random token from the platform CSPRNG
Existing system already keyed by v4Keep v4Mixing versions in one column is valid but gives no locality benefit for old rows; migrate only with measurements

Frequently Asked Questions

How can I tell a UUIDv4 from a UUIDv7 by looking at it?

Check the first character of the third group (character 15 of the 36-character string). It is the version nibble: 4 for UUIDv4, 7 for UUIDv7. The first character of the fourth group is the variant and is 8, 9, a or b for both versions. A v7 also starts with a 12-hex-digit millisecond timestamp, so v7 IDs generated close together share a long common prefix.

Is UUIDv7 less random or less secure than UUIDv4?

UUIDv7 has 74 random bits (12 in rand_a plus 62 in rand_b) versus 122 in UUIDv4, and it reveals the creation time to anyone who sees the ID. Both are unpredictable enough for identifiers, but neither should be used as a secret: RFC 9562 treats UUIDs as identifiers, not as unguessable tokens. If the timestamp is sensitive, or you need a secret, use v4 or a dedicated random token.

Should I use UUIDv7 as a database primary key?

It is the version RFC 9562 recommends for new time-based IDs ("Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible"). Because v7 sorts by creation time when compared as raw bytes, new rows land at the end of a B-tree index instead of at random positions, which is the main practical advantage over v4 for insert-heavy tables. Measure on your own workload before migrating existing keys.

Does Python or Node.js have a built-in UUIDv7 generator?

Python does from version 3.14: uuid.uuid7() is documented as generating a time-based UUID per RFC 9562 section 5.7 with a 48-bit timestamp and a 42-bit counter for monotonicity within a millisecond. On older Python versions and in Node.js (which ships crypto.randomUUID() for v4 only) you need a library or the small hand-rolled generator shown in this guide.

Try it in the browser

Generate v4 IDs with the UUID Generator (uses crypto.randomUUID() when available), and validate the format with the UUID v4 regex pattern page, which includes braced, compact and extraction variants.

Related guides