Remove adsMember
Theme: SystemContrast: Standard
On this page
  1. Where the version lives
  2. v1 tells on you
  3. v4 is just random
  4. Will two ever collide?
  5. What random keys do to a B-tree
  6. v7 puts the clock first
  7. Which one

UUID v1, v4 and v7: what leaks, what collides, what scales

By 7 min readExpert
Copy article
Share this article

A team uses UUID v4 as the primary key, ships, and is happy for a year. Then the table outgrows RAM. Insert latency grows a long tail and the write-ahead log balloons. The code is fine. The keys are doing what random keys always do to a B-tree.

Down the hall, another team puts v1 UUIDs in public URLs. They’re publishing the creation time of every record, plus a stable fingerprint of the machine that made it, and they don’t know it.

UUID versions aren’t interchangeable flavours of one thing. Each trades off uniqueness, secrecy and sort order differently, and RFC 9562 [1] (which replaced RFC 4122 [3]) spells out how. Which version you use is a privacy decision and a storage decision. Most people make it by accepting whatever the ORM defaults to.

Where the version lives

Every UUID is 128 bits, written as 32 hex digits in 8-4-4-4-12 groups. Two fields are the same in every version. The 4-bit version is the first hex digit of the third group. The 2-bit variant sits in the top bits of the fourth group: 10 for RFC-style UUIDs, which is why that digit is always 8, 9, a or b. That leaves 122 bits for content.

xxxxxxxx-xxxx-Vxxx-Nxxx-xxxxxxxxxxxx
              ^     ^
              |     variant: 8, 9, a or b
              version: 1, 4, 7, ...

What goes in those 122 bits is where the versions differ.

Version Content of the 122 bits Ordered by time Leaks
v1 60-bit timestamp, 14-bit clock sequence, 48-bit node Not lexically (low bits first) Creation time, node identity
v4 122 random bits No Nothing by design
v7 48-bit Unix ms timestamp, version, then 74 bits of random or counter data Yes Creation time to the millisecond

v1 tells on you

v1 counts 100-nanosecond ticks since 1582-10-15, the Gregorian calendar reform (lovely choice), in 60 bits. It adds a 14-bit clock sequence to survive the clock going backwards, and a 48-bit node ID, which traditionally is the MAC address of the machine that made it.

So anyone holding a v1 UUID can work out when it was created, and, if the node is a real MAC, on which machine. RFC 9562 explicitly allows a random node with the multicast bit set instead, and most modern libraries do that. Check yours.

The other problem: the timestamp is stored in a scrambled order. time_low comes first, then time_mid, then time_hi, so the fastest-changing bits lead and v1 values don’t sort by creation time, neither as strings nor as bytes. You pay the privacy cost of a timestamp and get none of the indexing benefit.

Decoding takes a few lines:

python
import uuid, datetime
u = uuid.UUID("c232ab00-9414-11ec-b3c8-9f6bdeced846")
# 100 ns ticks since 1582-10-15; offset to the Unix epoch
ticks = u.time - 0x01b21dd213814000
print(datetime.datetime.fromtimestamp(ticks / 1e7, datetime.timezone.utc))
print(hex(u.node))  # multicast bit set => random node, not a real MAC

If a v1 UUID has ever been in a URL, an API response or a log you shared, the creation time is public. Look at the lowest bit of the first node octet: if it’s set, the node ID is random and not a real MAC.

v4 is just random

122 bits from a cryptographically secure random generator, nothing else. No structure, so nothing to leak.

It’s only as good as its source, though. A non-cryptographic PRNG (Math.random() in older runtimes, a seeded rand()) makes the values predictable, and some very old libraries did exactly that. Use the OS CSPRNG.

And unguessable isn’t the same as safe to use for authorisation. RFC 9562 itself warns against treating UUIDs as security capabilities. For that you want a purpose-built token with an explicit expiry.

Will two ever collide?

For n random values out of N possibilities, with n much smaller than N, the chance of at least one duplicate [2] is about

p ≈ n² / (2N)

For v4, N = 2^122 ≈ 5.3 × 10^36. A trillion rows, n = 10^12:

p ≈ (10^12)² / (2 × 5.3×10^36) = 10^24 / 1.06×10^37 ≈ 9.4 × 10^-14

Roughly one in ten trillion. Getting to a 50% chance takes about n ≈ 1.18 × √N, around 2.7 × 10^18 UUIDs. That’s a billion a second for about 85 years. Your storage controller flipping bits is the likelier problem. And your unique constraint is the backstop anyway: a collision becomes a failed insert you retry, not silent corruption.

v7 is fine too, though the maths is a bit different. Its collision domain isn’t 2^122 but the random bits within one timestamp tick, 74 of them (RFC 9562 lets part of that be a monotonic counter). Two v7 IDs can only collide if they’re made in the same millisecond. A million IDs in one millisecond across the whole system gives p ≈ (10^6)² / (2 × 2^74) ≈ 10^12 / 3.8×10^22 ≈ 2.6 × 10^-11 for that millisecond. Rare, and the unique index catches it.

Birthday bound: collision probability versus number of IDs for v4 and for v7 within one millisecond

What random keys do to a B-tree

This is what actually hurts in production. A primary-key index is usually a B+tree: sorted pages, each holding a range of keys. Every insert has to find the leaf page its key belongs on.

With auto-increment or time-ordered keys, each new key is bigger than all the others, so every insert hits the rightmost leaf. That page stays hot in cache, pages fill up completely before they split, and writes are sequential.

With v4, each insert lands on a random leaf. Once the index is bigger than the buffer pool, most inserts have to read a page from disk first. Random inserts into full pages split them, typically leaving two halves about 50 to 70 percent full, so the index takes more space on disk and in memory. Each batch of transactions dirties many separate pages instead of one hot page, which means more WAL and more flushing.

Clustered engines get it worst. In InnoDB the primary key is the table, so random keys scatter the rows themselves, and every secondary index carries the wide key as well. PostgreSQL heap tables are unordered, so it’s milder there, but the primary key index loses locality just the same.

I won’t quote a speedup number. It depends on row width, buffer pool size and workload. Benchmark your own schema before promising anyone a figure.

Inserts into a B-tree: random v4 keys scatter across leaves, v7 keys append to the rightmost leaf

v7 puts the clock first

v7 puts a big-endian 48-bit Unix millisecond timestamp in the most significant bits, then the version nibble, then 74 bits of randomness. An implementation can use some of those as a counter so one process’s IDs stay monotonic within a millisecond. Since the timestamp comes first and the encoding is big-endian, plain byte or string comparison sorts v7 UUIDs by creation time, to the millisecond. Inserts become nearly append-only, and you still get globally unique IDs without a coordination service.

The downsides are real, though. Anyone with the ID knows when the record was created, to the millisecond. If a record’s age is sensitive (an account’s creation date, when a draft was started), v7 in a public identifier leaks something v4 doesn’t. The usual answer is v7 as the internal primary key and a separate random ID for public use.

Ordering only holds per millisecond. Within one, it depends on the counter or the randomness, and clocks on different machines drift, so global order is approximate. An NTP step backwards can produce IDs smaller than ones already handed out; good libraries clamp to the last timestamp they saw. And the timestamp part is guessable, so only the 74 random bits resist enumeration. Fine for keys, wrong for secrets.

Generate a few v7 IDs in the UUID generator and sort them as plain strings: they stay in creation order. Try the same with v4 and there’s no order to keep.

Which one

Situation Pick
Database primary key, high insert rate, table larger than memory v7
Identifier exposed publicly where creation time must stay private v4 (or v7 internally plus a random public ID)
Existing v1 data Keep it, but treat the timestamp as public and the node as a fingerprint
Anything used as a secret or bearer token Neither: use a dedicated random token with expiry

Whichever you pick, store UUIDs as 16-byte binary where your database supports it (a native uuid type, or BINARY(16)), not as 36-character strings. That more than halves the key, and the saving repeats in every secondary index.

Try it yourself

UUID and ULID generator

Generate UUID v4, UUID v7 and ULID in bulk, or paste an ID to see its version, variant and timestamp.

Open the tool

Share this article

Share this article

Sources

  1. RFC 9562: Universally Unique IDentifiers (UUIDs)1
  2. Birthday problem (reference)1
  3. RFC 4122 (obsoleted by RFC 9562)1

Keyboard shortcuts

Close

Everywhere

Search tools and actions
Ctrl K
Show this help
?
Switch theme
Ctrl Shift L
Pin the selected tool (in search)
Ctrl P

On a tool

Send the output to the first suggestion
Ctrl ↵
Open a file into the input
Ctrl O
Copy the output
Ctrl Shift C
Close searchesc
toolsBase64 encoder and decoderEncode text to Base64 or decode Base64 back to text, with standard and URL-safe alphabets.EncodingJSON formatter and validatorFormat, minify and validate JSON, with the exact line and column of any error.FormattingRegex testerTest JavaScript regular expressions with live highlighting, capture groups and a plain-language explanation.TextUUID and ULID generatorGenerate UUID v4, UUID v7 and ULID in bulk, or paste an ID to see its version, variant and timestamp.GeneratorsUnix timestamp converterConvert Unix timestamps to dates and back, in any time zone, with ISO 8601, RFC 2822 and relative time.Date and timeHash and HMAC generatorCalculate MD5, SHA-1, SHA-2, SHA-3, BLAKE2, BLAKE3 and CRC-32 hashes of text or a file, with optional HMAC.SecurityPassword generator and strength checkerGenerate random passwords, passphrases and PINs, or check how strong a password is.SecurityJWT decoder and verifierDecode a JSON Web Token, check its expiry and claims, and verify its signature with a secret or public key.SecurityCIDR and IP subnet calculatorCalculate the range, mask and size of IPv4 and IPv6 networks, split them into subnets, and check what contains or overlaps what.NetworkCron expression explainerRead a cron expression in plain language, see its next runs in any time zone, and tidy it into its standard form.Date and timeSort linesSort lines of text A to Z or Z to A, in natural order (file2 before file10), by code point, by number or by length.TextRemove duplicate linesRemove duplicate lines from a list, keep the first or last copy, ignore case or spaces, or drop every line that repeats.TextCompare lists and textCompare two lists line by line: find what is in both, only in one, or in either, or see a marked line diff side by side.TextText case converter and slugifyConvert text to camelCase, snake_case, kebab-case, Title Case and more, or turn a title into a URL slug with accents handled.TextURL encode, decode and parsePercent-encode and decode text for URLs, or take a URL apart into scheme, host, path and query and edit the query as a table.EncodingValidators: IBAN, BSN, VAT, card and ISBNCheck an IBAN, Dutch BSN, credit card number, EU VAT number, email address, URL or ISBN, with the reason when it fails. One value per line.ValidationHTML entity encoder and decoderEscape text for HTML with named, decimal or hex entities, or turn entities such as & and é back into characters.EncodingEscape and unescape stringsEscape text for a JSON or JavaScript string, a SQL string, a CSV field, a regular expression or a shell word, or undo the escaping.EncodingEAN, UPC and GTIN check digitsCheck the check digit of an EAN-8, EAN-13, UPC-A or GTIN-14, add a missing one, read the country from the GS1 prefix and convert between ISBN-10 and EAN-13. One code per line.ValidationTest data generatorGenerate valid-looking test IBANs, BSNs, EAN barcodes and EU VAT numbers in bulk. They pass the checks, but belong to no one.GeneratorsSQL formatter and minifierFormat SQL so each clause sits on its own line, or minify it onto one line, for MySQL, PostgreSQL, SQLite, SQL Server and BigQuery.FormattingNumber base converterConvert numbers between binary, octal, decimal, hex and any base from 2 to 36, with big numbers, two's complement and a bit view.EncodingYAML, TOML and JSON converterConvert between JSON, YAML and TOML, or validate and pretty-print a document, with the line and column of every error.FormattingCSV and JSON converterConvert CSV to a JSON array of objects or arrays, or JSON back to CSV. Detects the delimiter and reads Dutch Excel files with semicolons and decimal commas.DataGlob and .gitignore testerTest glob patterns and .gitignore rules against a list of paths and see which paths match and which rule matched them.TextColour converterConvert a colour between HEX, RGB, HSL, HWB and OKLCH, check its WCAG contrast against a background, or build a Material 3 tonal palette from it.Formattingchmod calculatorConvert Unix file permissions between octal (755), letters (rwxr-xr-x), ls -l and chmod clauses, with a checkbox grid and the setuid, setgid and sticky bits.SecurityUser-agent parserRead a user-agent string: browser, engine, OS, device type and whether it is a bot. Leave it empty to read your own browser.NetworkConvert cURL to codePaste a cURL command and get the same request as fetch, Axios, Python requests, Go or PHP. Nothing is run and nothing is sent.NetworkMarkdown previewWrite Markdown and see it rendered as you type, or copy the HTML. Raw HTML is shown as text and links are checked, so nothing in the preview can run.FormattingBarcode and QR code generatorMake a QR code, EAN-13, UPC-A, Code 128, ITF-14 or Data Matrix barcode, download it as SVG or PNG, or read a code from an image.GeneratorspagesChangelogWhat changed in each releasepagesAboutWhat codeknife.dev is and who makes itpagesIdeasSuggest improvements and vote on other visitors’ ideas.pagesPrivacyWhat leaves your browser and what is storedpagesAccessibilityConformance, known limitations and how to report a problempagesMembershipRemove ads and raise the limitspagesactions