Will Your Note IDs Ever Collide? The Birthday Math
Reference: RFC 9562 — Universally Unique IDentifiers (UUIDs) — IETF Standards Track · 2024
A random UUID v4 collision is not going to happen in your note vault. Version 4 carries 122 random bits, so you would need on the order of 2.71 quintillion IDs before a coin-flip chance of a single clash.12 If you see a collision, you found a bug, not bad luck.
That distinction is the whole post. Once you give every note a stable identifier, the next question is whether two of them could ever end up the same, and the internet is full of confident, contradictory answers. The math is not actually hard, and it settles the argument cleanly: for a properly generated random ID the odds are so far below anything physical that "impossible in practice" is a fair summary. But that same certainty is a diagnostic tool. When a duplicate does show up, the probability math tells you exactly where not to look (bad luck) and where to look instead (how the IDs were minted). This piece derives the bound, then walks the real failure modes, and ends with the ID scheme a local-first Markdown vault should actually use.
What actually happened on Hacker News?
In May 2026 a developer opened an Ask HN thread that reached 479 points with a simple, disbelieving claim: their database had flagged a duplicate UUID v4.3 A record from 2025 and a fresh insert produced the exact same identifier, b6133fd6-70fe-4fe3-bed6-8ca8fc9386cd, in a table of only about 15,000 rows. "Statistically... impossible. Has that ever happened to anyone?! What in the..." the poster wrote.3
The top replies did not reach for luck. "I'd be suspecting a race condition or some other naive mistake, otherwise id be stocking up on lottery tickets," one wrote.3 That is the correct instinct, and the math below is why. A duplicate in 15,000 random 122-bit values is not a rare event you happened to witness. It is evidence that the values were not what you thought they were.
How many bits are actually in a UUID?
A UUID is 128 bits wide, but not all of them are random. RFC 9562, the 2024 standard that defines modern UUIDs, describes version 4 as filling "random_a, random_b, and random_c (122 bits total)" with random data, then overwriting six bits for the version and variant markers.1 So the entropy that protects you from collisions is 122 bits, not 128. That is still an enormous number: 2 to the 122nd power is about 5.3 × 10^36 distinct values.
ULID and UUID version 7 spend some of those bits on a timestamp instead. A ULID is a 48-bit millisecond timestamp plus "80 bits" of randomness, encoded as a 26-character string.4 UUID v7 "allocat[es] a Unix timestamp in milliseconds in the most significant 48 bits and fill[s] the remaining 74 bits" with random data (minus the version and variant bits).1 Fewer random bits means a smaller haystack, which matters only in a way we will make precise later. First, the headline number.
The birthday math, derived
You do not need 2^122 IDs for a 50 percent chance of a collision. You need roughly the square root of that, which is the entire surprise of the birthday problem. For a space of N equally likely values, the probability that n draws contain at least one duplicate is well approximated by p(n) ≈ 1 − e^(−n² / 2N), and the count that makes p equal one half is n ≈ 1.1774 × √N.
Plug in N = 2^122 and you get the number everyone quotes. Wikipedia's UUID article states it directly: "the number of random version-4 UUIDs which need to be generated in order to have a 50% probability of at least one collision is 2.71 quintillion," which it also frames as "generating 1 billion UUIDs per second for about 86 years."2 Here is the same result from first principles, small enough to paste into a REPL:
from math import log, sqrt, expm1
BITS = 122 # UUID v4 random bits (RFC 9562)
N = 2 ** BITS # size of the ID space
def collision_p(n): # birthday approximation, numerically stable
return -expm1(-(n * n) / (2 * N)) # == 1 - exp(-n^2/2N)
def fifty_percent_at(): # n for p = 0.5
return sqrt(2 * log(2) * N)
print(f"space size : {N:.3e}") # 5.317e+36
print(f"50% collision at n : {fifty_percent_at():.3e}") # 2.715e+18
print(f"p at 1e9 UUIDs : {collision_p(1e9):.3e}") # 9.40e-20
print(f"p at 1e12 UUIDs : {collision_p(1e12):.3e}") # 9.40e-14
Read the last two lines slowly. Mint a billion v4 UUIDs and your chance of even one collision is about 9.4 × 10^-20, which is roughly one in ten quintillion. Mint a trillion and it is still about 9.4 × 10^-14. To reach a modest one-in-a-billion risk you need around 103 trillion UUIDs; Wikipedia puts the same figure as "the probability to find a duplicate within 103 trillion properly-generated version-4 UUIDs is one in a billion."2 A 15,000-row table is not within astronomical distance of these numbers. The probability that 15,000 correctly generated v4 UUIDs contain a duplicate is about 2 × 10^-29. You will not witness that.
So a random collision is off the table — what causes a real one?
If the math forbids luck, an observed collision has a cause, and the cause is in how the IDs were generated. There are five usual suspects, and each maps to a specific place in the code. Work down them in order; the first one that matches is almost always the answer, and none of them is fixed by choosing a "bigger" ID.
The five causes, from most to least common:
- Truncation. The generator produced a full 122-bit UUID and something downstream stored only part of it: a
VARCHAR(8)column, aslice(0, 8)for a "friendly" filename, a display helper that leaked into a key. Eight hex characters is 32 bits, and 32 bits collides after roughly 77,000 IDs, not 2.71 quintillion. - A weak random source. The code called
Math.random()instead of a cryptographic generator. MDN is blunt: "Math.random() does not provide cryptographically secure random numbers. Do not use them for anything related to security. Use the Web Crypto API instead."5 A weak or poorly seeded PRNG can repeat, especially across many devices seeded from similar clocks. - A duplicated seed, or a copied record. Two processes seeded the same PRNG identically (common after a
fork()without reseeding), or a template note was duplicated with itsid:field intact, or an import re-used source IDs. The IDs are not colliding; they are copies. - Two generation sources that drifted. The Hacker News poster's own follow-up is the textbook case: their IDs "used to [be] generated on-device by users, and for many months now, has moved to being generated on server."3 Two code paths, two libraries, one namespace: a fine setup for a duplicate that no single generator would ever produce.
- A clock or counter bug in a time-ordered ID. ULID and UUID v7 embed a timestamp, so IDs minted in the same millisecond share a prefix and lean on far fewer random bits. A broken monotonic counter, a clock that runs backwards, or a rollover can manufacture a duplicate that pure v4 never would.
flowchart TD
A[Two notes share<br/>one ID] --> B{Full ID stored,<br/>never truncated?}
B -->|No| C[Truncation bug:<br/>fix the schema]
B -->|Yes| D{CSPRNG source,<br/>not Math.random?}
D -->|No| E[Weak RNG:<br/>swap in crypto]
D -->|Yes| F{One generator,<br/>no shared seed?}
F -->|No| G[Duplicate seed<br/>or copied record]
F -->|Yes| H{Time-ordered ID,<br/>same millisecond?}
H -->|Yes| I[Clock or counter bug:<br/>use monotonic factory]
H -->|No| J[Suspect a bit-flip,<br/>not bad luck]
Figure: The diagnostic tree for an observed collision. Start at the top and answer each question about how the ID was minted and stored; the first "No" points at the bug. Only when the full ID was stored, generated by a cryptographic source, from a single un-shared generator, and is not time-ordered do you reach the leaf where a hardware bit-flip is more plausible than a generation error. Each branch is a specific fix, and none of them is "use a bigger ID."
The single most preventable cause is the first one. The code below shows the anti-pattern and the fix side by side. Note the rule embedded in it: never truncate an identifier you intend to use as a key.
import { randomUUID } from "node:crypto"; // CSPRNG-backed
// WRONG: a "friendly" short id. 8 hex chars = 32 bits.
// Collides ~50% after ~77,000 notes (birthday bound on 2^32).
const badId = randomUUID().slice(0, 8); // "b6133fd6"
// RIGHT: store the whole 122-bit value. Derive display forms,
// never keys, from a prefix.
const noteId = randomUUID(); // full UUID, the key
const shortLabel = noteId.slice(0, 8); // for the UI only, never a key
The npm uuid library the Hacker News poster used is explicit that it "Uses modern crypto API for random values,"6 and browsers expose the same guarantee: "The Crypto.getRandomValues() method lets you get cryptographically strong random values."7 When the source is a real CSPRNG and the full value is stored, causes 1 through 3 are closed by construction, which is why the remaining suspects are almost always a second generation path or a time-ordered edge case.
Sortable IDs for a note vault: ULID and UUID v7
For a folder of Markdown files, sortability is worth more than raw entropy, and that is where ULID and UUID v7 earn their place. Both embed a 48-bit millisecond timestamp in the high bits, so IDs sort in creation order by default, which makes filenames, indexes, and database inserts friendlier. ULID is the more filesystem-native of the two: 26 Crockford-base32 characters versus the UUID's 36, case-insensitive and URL-safe, and it "Won't run out of space 'til the year 10889 AD."4
The one place a time-ordered scheme is genuinely more delicate is the same millisecond. Because ULID spends only 80 bits on randomness per millisecond, two IDs minted in the same tick draw from a 2^80 space, not 2^122. That is still 1.21 × 10^24 possibilities per millisecond,4 so a same-millisecond clash would need on the order of 1.3 trillion IDs inside one tick before a coin flip. The spec closes even that gap deterministically with a monotonic factory:
import { monotonicFactory } from "ulid";
const ulid = monotonicFactory();
// Within the same millisecond, the random component is
// incremented by one bit (with carry), so order is preserved
// and the values cannot repeat inside the tick.
ulid(); // 01BX5ZZKBKACTAV9WEVGEMMVRZ
ulid(); // 01BX5ZZKBKACTAV9WEVGEMMVS0
The ULID spec describes this precisely: "if the same millisecond is detected, the random component is incremented by 1 bit in the least significant bit position (with carrying)."4 It also names the only real failure mode: "If, in the extremely unlikely event that, you manage to generate more than 2^80 ULIDs within the same millisecond, or cause the random component to overflow with less, the generation will fail."4 It fails loudly rather than colliding silently, which is exactly the behavior you want. UUID v7 offers the same monotonicity story through RFC 9562's counter methods, and the RFC warns against the naive version: "incrementing the counter by 1 SHOULD NOT be used by implementations that favor unguessability, as the resulting values are easily guessable."1
Which ID should a local-first note vault use?
Prefer a time-ordered, filesystem-friendly ID: ULID if you want the shorter, sortable, URL-safe string, or UUID v7 if you want to stay inside the UUID ecosystem your tools already understand. Reach for plain UUID v4 only when you specifically do not want the creation time to be inferable from the ID. All three are collision-safe when generated correctly; the differences are about sorting and readability, not safety.
| Scheme | Random bits | Chars | Sortable | Same-millisecond safety | Best for |
|---|---|---|---|---|---|
| UUID v4 | 122 | 36 | No | independent random draws | when creation time must stay hidden |
| UUID v7 | 74 (+ 48-bit ms) | 36 | By time | counter/random monotonic (RFC 9562)1 | staying in the UUID ecosystem |
| ULID | 80 (+ 48-bit ms) | 26 | Lexical | monotonic factory, +1 bit4 | short, sortable, filename-friendly keys |
For a vault of Markdown files whose IDs double as filenames and heading anchors, ULID's 26-character sortable string is the ergonomic winner. But the safety of every row in that table rests on one rule: store the whole value. A time-ordered scheme does not save you from truncation, and neither does a bigger random field.
Trade-offs and honest caveats
"Astronomically unlikely" is not "impossible," and it is worth saying so plainly. A correctly generated random ID can, in principle, repeat; the point is that the probability is so far below other risks that it is the wrong thing to engineer against. Wikipedia makes the sharper version of this point: past a certain scale, "duplicate UUIDs are more likely to be the result of bit-flips ... caused by cosmic rays passing through memory or disk storage, than the result of mischance at UUID-generation time."2 In other words, if you have genuinely ruled out every generation bug, the next suspect is not your ID scheme but your storage integrity, which is a different property worth checking separately.
Time-ordered IDs carry their own trade-off: because the timestamp is embedded, a ULID or UUID v7 leaks the approximate moment a note was created to anyone who can read the ID. For a private local-first vault that is usually a feature (free creation-order sorting); if your IDs are exposed publicly and creation time is sensitive, v4's opacity is the safer default. And every scheme here assumes a real cryptographic source. On a platform without a CSPRNG, none of these guarantees hold, which is the one caveat RFC 9562 itself flags around its unguessability advice.1
Frequently Asked Questions
Can UUID v4 collide? In theory yes, in practice no. Version 4 has 122 random bits, so a 50 percent chance of a single collision requires about 2.71 quintillion UUIDs, equivalent to a billion per second for roughly 86 years.12 At any personal or even planetary note count the probability is effectively zero.
Will my note IDs ever collide by chance? No. For correctly generated random IDs the odds are below 10^-30 at any realistic vault size. If you observe a duplicate, it is a generation bug (truncation, a weak random source, a copied record, two generators, or a clock bug), not bad luck. Audit how IDs are minted.3
Should I use UUID or ULID for my notes? For a Markdown vault, prefer ULID: 26 sortable, URL-safe characters that order by creation time and make friendly filenames.4 Use UUID v7 to stay inside the UUID ecosystem, or plain UUID v4 when you specifically do not want creation time inferable from the ID.
Is UUID v7 better than v4? For a note vault, usually. V7 embeds a 48-bit millisecond timestamp so IDs sort by time and index efficiently, trading some randomness (74 random bits) for order.1 V4's 122 random bits are only preferable when you want the ID to reveal nothing about when the note was created.
Is Math.random() good enough for IDs? No. MDN states that "Math.random() does not provide cryptographically secure random numbers" and directs you to the Web Crypto API.5 A weak PRNG can repeat or be seeded identically across processes, which is a real, documented cause of duplicate IDs. Use crypto.randomUUID() or crypto.getRandomValues().7
Why did I get a duplicate UUID? Almost always one of: the ID was truncated before storage, generated with a non-cryptographic source, copied along with a duplicated record, produced by two different generators sharing a namespace, or minted by a broken clock/counter in a time-ordered scheme.3 Fix the minting path, not the ID length.
Do ULIDs collide within the same millisecond? Only in the extreme. Each millisecond has 80 random bits (1.21 × 10^24 values),4 and the monotonic factory increments the random component by one bit within a tick so order is preserved and values cannot repeat; it fails loudly if you exhaust the space.4 For any real workload this is a non-issue.
References
A collision you can see is a message about your code, not your luck: read it, fix the mint, and leave the ID scheme alone.
Keep your notes in plain Markdown files you fully own, on your own device — mnmnote.com.
Footnotes
-
RFC 9562, "Universally Unique IDentifiers (UUIDs)," IETF, 2024 — §5.4 (v4 "122 bits total"), §5.7 (v7 48-bit timestamp + 74 remaining bits), §6.2 (monotonic counters), §6.9 (CSPRNG unguessability). https://www.rfc-editor.org/rfc/rfc9562 · Wayback https://web.archive.org/web/20260517204516/https://www.rfc-editor.org/rfc/rfc9562 · accessed 2026-07-21. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
"Universally unique identifier," Wikipedia — collision section: "2.71 quintillion" for a 50% collision, "1 billion UUIDs per second for about 86 years," "103 trillion ... is one in a billion," and the cosmic-ray bit-flip comparison (cites Mathis, SIAM Review 33(2):265–270, 1991, DOI 10.1137/1033051). https://en.wikipedia.org/wiki/Universally_unique_identifier · accessed 2026-07-21. ↩ ↩2 ↩3 ↩4 ↩5
-
"Ask HN: We just had an actual UUID v4 collision..." (479 points, 2026-05-08), Hacker News item 48060054 — poster
mittermayr("Statistically... impossible"; the on-device → server generation change), replyserf("a race condition or some other naive mistake"). https://news.ycombinator.com/item?id=48060054 · Wayback https://web.archive.org/web/20260611034800/https://news.ycombinator.com/item?id=48060054 · accessed 2026-07-21. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
ULID specification, ulid/spec — 48-bit ms timestamp + 80 random bits, 26 Crockford-base32 chars, "1.21e+24 unique ULIDs per millisecond," "Won't run out of space 'til the year 10889 AD," and the monotonicity/overflow rules. https://github.com/ulid/spec · Wayback https://web.archive.org/web/20260712145825/https://github.com/ulid/spec · accessed 2026-07-21. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
"Math.random()," MDN Web Docs — "Math.random() does not provide cryptographically secure random numbers. Do not use them for anything related to security. Use the Web Crypto API instead." https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random · accessed 2026-07-21. ↩ ↩2
-
uuidjs/uuid README — "Secure - Uses modern
cryptoAPI for random values." https://github.com/uuidjs/uuid · accessed 2026-07-21. ↩ -
"Crypto.getRandomValues()," MDN Web Docs — "The Crypto.getRandomValues() method lets you get cryptographically strong random values." https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues · accessed 2026-07-21. ↩ ↩2