All posts

UUID v4 vs v7: which one should you use?

UUID v4 is 122 random bits; v7 starts with a millisecond timestamp so keys sort by time. Layout, collision maths, index impact, storage and when to use each.

By Last reviewed 7 min readUUIDDatabasesIdentifiers

Every UUID is 128 bits, but how those bits are filled decides how the ID behaves. UUID v4 is almost entirely random. UUID v7 starts with its creation time in milliseconds, so newer IDs sort after older ones. For most new primary keys v7 is the better default; v4 is the choice when an ID must not reveal anything, including when it was made.

To follow along, open the UUID generator and inspector. It generates v1, v3, v4, v5, v7 and Nil UUIDs in your browser, and its Inspect panel decodes any UUID you paste: version, variant and, for v1, v6 and v7, the creation time.

What is inside a UUID

A UUID is 32 hex digits in five groups, 8-4-4-4-12. Two fields sit at fixed positions in every standard UUID:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

M  version: the 13th hex digit (4 for v4, 7 for v7)
N  variant: its top two bits are 10, so N is 8, 9, a or b

The other 122 bits belong to the version. The rules live in RFC 9562, published in May 2024. It obsoletes RFC 4122, keeps versions 1 to 5, adds 6, 7 and 8, and defines two special values: the Nil UUID (all zeros) and the Max UUID (all f).

VersionBuilt fromSorts by time
v160-bit count of 100 ns ticks since 1582, clock sequence, node IDNo
v3 / v5MD5 / SHA-1 hash of a namespace UUID plus a nameNo
v4122 random bitsNo
v6v1's time fields reordered, most significant firstYes
v748-bit Unix milliseconds, then 74 random or counter bitsYes
v8Whatever the application definesDepends

UUID v4: 122 random bits

A v4 sets the version and variant and fills everything else at random: 128 − 4 − 2 = 122 bits, or 2¹²² ≈ 5.3 × 10³⁶ possible values. The real question is whether any pair in your data set collides: the birthday problem. For n IDs drawn from N values:

p ≈ 1 − e^(−n² / 2N)          N = 2^122 ≈ 5.32 × 10^36

n = 10^9 (a billion rows)  p ≈ 10^18 / 1.06 × 10^37 ≈ 9.4 × 10^-20
p = 50%                    n ≈ 1.177 × √N = 1.177 × 2^61 ≈ 2.71 × 10^18
p = one in a billion       n ≈ √(2N × 10^-9) ≈ 1.03 × 10^14

In plain terms: even odds of a single duplicate takes 2.71 quintillion IDs, a billion per second for 86 years, and you can create about 103 trillion before the risk reaches one in a billion. All of this assumes a secure random source, as crypto.randomUUID() and Python's uuid.uuid4() use; a generator built on Math.random() voids the maths.

A v4 also reveals nothing. RFC 9562's security section says that if UUIDs are used in any security operation, v4 SHOULD be the version.

UUID v7: a timestamp first, then randomness

  48 bits      4 bits   12 bits   2 bits   62 bits
unix_ts_ms  |  ver=7  | rand_a  | var=10 | rand_b

The first 48 bits are milliseconds since 1970-01-01 UTC, stored big-endian, so a later UUID is numerically larger. That field lasts until the year 10889. The other 74 bits are random in the simplest generators, but RFC 9562 lets an implementation spend some of them on a sub-millisecond time fraction (up to 12 bits) or a counter, so IDs made in the same millisecond stay in order.

Decoding a real v7

One from the devprix generator:

01a18bf3-7d4a-7608-8094-121ade4d5851

01a18bf3-7d4a      unix_ts_ms (48 bits)
7                  version 7
608                rand_a (12 bits, random here)
8                  binary 1000: variant 10, then 2 bits of rand_b
094-121ade4d5851   the other 60 bits of rand_b

0x01a18bf37d4a = 1793349352778 ms = 2026-10-30T08:35:52.778Z
// JavaScript
const ms = parseInt(id.replace(/-/g, "").slice(0, 12), 16); // 1793349352778
new Date(ms).toISOString();                                  // "2026-10-30T08:35:52.778Z"

# Python
uuid.UUID(id).int >> 80                                     # 1793349352778

Or paste 1793349352778 into the timestamp converter to see the milliseconds as a date. Thirteen digits means milliseconds, not seconds; Unix seconds vs milliseconds explains the difference.

What the timestamp gives away

Anyone who sees a v7 can read when it was made, to the millisecond. Put v7 IDs in public URLs and you publish when each account, order or document was created. RFC 9562 says embedded timestamps "do pose a very small attack surface". The first 48 bits are also guessable, so a v7 has 74 unpredictable bits at most, against 122 in a v4.

Nor is the embedded time a reliable created_at: a client can generate a v7 with any timestamp.

Why v7 is easier on B-tree indexes

A primary key index is a B-tree: keys kept in sorted order across fixed-size pages (8 kB in PostgreSQL, 16 kB by default in InnoDB). Where a new key goes depends only on its value.

  • v4 inserts land anywhere. Each new key belongs on a random leaf page. Once the index outgrows memory, many inserts must first read a page from disk, and full pages split into half-full ones all over the tree.
  • v7 inserts land at the right edge. New keys go to the last leaf page, which stays in memory; older pages fill up and are left alone, as with an auto-increment key.

Per MySQL's documentation, InnoDB pages filled in sequential order end up about 15/16 full; random inserts leave them 1/2 to 15/16 full. InnoDB also stores the table rows in primary key order, and every secondary index carries a copy of the primary key. In PostgreSQL, scattered inserts also mean more WAL: with full_page_writes on (the default), the first change to each page after a checkpoint logs the whole page.

The cost: under heavy concurrent inserts the last page becomes a hot spot, as with a sequence. And when the whole index fits in memory the gap narrows, so measure your own workload before rewriting keys.

v1, v3 and v5 in brief

  • v1 adds a 48-bit node ID, traditionally the MAC address, so it reveals where an ID was made as well as when. Its low time bits come first, so it doesn't sort by time. MySQL's UUID() returns v1. Browsers can't read a MAC address, so the devprix generator uses a random node with the multicast bit set.
  • v3 and v5 hash a namespace UUID and a name. Same input, same output: the DNS namespace plus example.com always gives cfbff0d1-9375-5685-968c-48ce8b15ae17 as v5. Use them for stable IDs derived from natural keys; anyone who can guess the name can compute the ID.

Storing UUIDs in a database

StorageSizev7 sorts by time?
PostgreSQL uuid16 bytesYes, compared byte by byte
MySQL BINARY(16) via UUID_TO_BIN(id)16 bytesYes
MySQL BINARY(16) via UUID_TO_BIN(id, 1)16 bytesNo
CHAR(36) or text36 characters plus overheadOnly if case and format are consistent

PostgreSQL 18 (September 2025) added uuidv7() and a uuidv4() alias for gen_random_uuid(), and its uuid_extract_timestamp() reads the time from v1 or v7 (version 17's read only v1):

CREATE TABLE orders (
  id         uuid PRIMARY KEY DEFAULT uuidv7(),
  created_at timestamptz NOT NULL DEFAULT now()
);

SELECT uuid_extract_timestamp('01a18bf3-7d4a-7608-8094-121ade4d5851'::uuid);
-- 2026-10-30 08:35:52.778+00   (with TimeZone = UTC)

In MySQL, generate v7 in your application and store it with UUID_TO_BIN(id). The swap flag in UUID_TO_BIN(id, 1) exists for v1: it moves the third group to the front, which for a v7 is the version digit and random bits, so rows sort randomly. Text keys take more than twice the space, repeated in every InnoDB secondary index.

ULID: the same idea, different text

A ULID is also 128 bits with a 48-bit millisecond timestamp first, then 80 random bits (it has no version or variant field). The difference is the text: 26 case-insensitive, sortable characters of Crockford Base32 (5 bits each, where Base64 uses 6) instead of 36 hex digits and dashes. The example's millisecond becomes the ULID prefix 01M65Z6ZAA. Its 16 bytes fit a uuid column, though UUID inspectors will usually flag it as non-standard.

Which one should you use?

  • Primary keys in a table that keeps growing: v7.
  • IDs in URLs where creation time is private: v4, or a v7 primary key plus a separate v4 public ID.
  • Secret-like values (invite or reset links): v4 from a secure generator, never v7.
  • The same ID for the same input every time: v5.
  • An existing v1 system that needs sortable IDs: v6, though RFC 9562 says to prefer v7 where possible.
  • Short, URL-friendly text IDs: ULID.
  • Request IDs, small tables, test data: either; v4 is simplest.

Pitfalls

String sorting works, with conditions

Text v7 IDs sort by time because fixed-width hex compares like a number: in ASCII, 0–9 come before a–f. Mixing cases breaks it (B sorts before a), as does mixing dashed and undashed forms, so normalise on the way in.

Same-millisecond order depends on the generator

One click in the devprix generator produced these three, in this order:

01a18bf3-7d4a-7608-8094-121ade4d5851
01a18bf3-7d4a-7ad0-bb96-45d533aa57a7
01a18bf3-7d4a-7018-95d1-efaac63d9c1a

All three share the millisecond 01a18bf3-7d4a, and this generator fills the other 74 bits at random with no counter, so sorting puts the third one first. Python 3.14's uuid.uuid7() uses a 42-bit counter and PostgreSQL 18 a sub-millisecond fraction, so each stays in order within one process or session. Across processes and machines nothing orders IDs within a millisecond, and clocks drift; if you need strict order, use a sequence.

Next steps

Generate a few v7 IDs in the UUID generator, paste one into Inspect and compare its Unix milliseconds with the moment you clicked. To decide how to show that time in an API, read Unix timestamps vs ISO 8601.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.