UUID Collision Stories: When Astronomically Unlikely Still Happens

"You'd need to generate 103 trillion UUIDv4 values to have a one-in-a-billion chance of a collision" is true, and also somewhat beside the point. Real collision incidents do happen in production systems — just almost never for the reason people assume. This post works through the actual birthday-paradox math so you have the real numbers, then breaks down the categories of root causes behind reported UUID collisions, none of which are "the random number generator got unlucky." The scenarios below are illustrative composites drawn from patterns that recur across bug trackers, incident postmortems, and RNG security research — not claims about specific named companies — because the mechanisms are the useful part, not the attribution.


The Math, Precisely

A UUIDv4 has 122 bits of randomness (128 total bits, minus 6 fixed version/variant bits). The birthday paradox gives the probability of at least one collision after generating k values from a space of size N = 2^122:

P(collision) ≈ 1 - e^(-k² / 2N)
function collisionProbability(bits, count) {
  const N = Math.pow(2, bits);
  const k = BigInt(count);
  // Using the k²/2N approximation, valid when k << sqrt(N)
  return (Number(k) ** 2) / (2 * N);
}

collisionProbability(122, 1_000_000_000);      // ≈ 1.06e-19
collisionProbability(122, 103_000_000_000_000); // ≈ 1.09e-9  — the "one in a billion" figure
IDs Generated Approx. Collision Probability
1 million ~1 in 9.4 × 10^30
1 billion ~1 in 9.4 × 10^24
1 trillion ~1 in 9.4 × 10^18
103 trillion ~1 in 1 billion
2.71 × 10^18 (~2.7 quintillion) ~50% (the birthday bound)

At any volume a single application is realistically going to produce, true random UUIDv4 collision probability rounds to zero in every meaningful sense. So when teams do report duplicate UUIDs, the random-number math almost never turns out to be the actual cause once someone investigates. It's a symptom that points to something else being broken.


Category 1: The RNG Wasn't Actually Random

The 122-bits-of-entropy guarantee assumes a cryptographically secure random number generator feeding the UUID function. Plenty of "UUID-like" generators, and even some real UUID library implementations in older ecosystems, didn't use one.

// A pattern still found in legacy codebases: PRNG-backed pseudo-UUIDs
function legacyUuid() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const r = Math.random() * 16 | 0; // V8's xorshift128+, NOT cryptographic
    return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
  });
}

Math.random()'s internal state is a fixed-size seed, not a full-entropy CSPRNG stream. Under enough concurrent callers — many worker processes started close together, or a PRNG reseeded from correlated inputs — the effective entropy is far below 122 bits, and the birthday math above simply doesn't apply anymore, because its precondition (uniform randomness across the full space) was never true to begin with.

The fix is always the platform's CSPRNG-backed generator:

const id = crypto.randomUUID(); // RFC 9562 compliant, backed by the OS CSPRNG
Guid id = Guid.NewGuid(); // .NET's implementation uses a CSPRNG internally

Category 2: Low Entropy at Process/VM Boot

This is a well-documented class of security and reliability issue, independent of UUIDs specifically: early in a virtual machine or container's boot sequence, the OS's entropy pool may not yet have accumulated enough unpredictable input (interrupt timing, disk I/O jitter, etc.) to seed a CSPRNG properly. Research into embedded and cloud-instance key generation (e.g., the widely cited "Mining Your Ps and Qs" study on weak cryptographic keys) found real-world cases of devices generating predictable random output shortly after boot, precisely because of insufficient entropy at that moment.

If a service generates UUIDs immediately on cold start — before the OS's CSPRNG has mixed in enough real entropy — and does so across many identical container instances that all booted from the same image at nearly the same time, the resulting "random" values can be far more correlated than the math assumes. This isn't a UUID-library bug; it's an environmental precondition the library's correctness depends on but can't verify.

Mitigations:

// Warm up / avoid ID generation on the hot path during process startup
// in extremely entropy-sensitive environments; most modern platforms
// (Linux 3.17+ getrandom(), modern container runtimes) have largely
// closed this gap, but it's worth verifying for embedded/legacy targets.
# Diagnostic: check available entropy on Linux
cat /proc/sys/kernel/random/entropy_avail

On modern Linux kernels using getrandom() (which blocks until the CSPRNG is properly seeded, rather than returning weak output), this specific failure mode is largely closed — but it's exactly the kind of assumption worth verifying explicitly if you're targeting older kernels, embedded systems, or unusual container base images.


Category 3: UUIDv1/v6 Node ID Duplication from Cloned VMs

UUIDv1 embeds a 48-bit "node identifier," historically a MAC address. Virtualization platforms that clone a VM template without regenerating the virtual network adapter can produce multiple running instances with the identical virtual MAC address — a well-documented operational pitfall independent of UUIDs, but one that directly undermines UUIDv1's uniqueness assumption when combined with clock resolution limits.

// If two cloned VMs share a MAC-derived node ID, and both generate
// a UUIDv1 within the same clock tick / sequence window, the
// (timestamp, clock_seq, node) triple can collide.

The fix: don't rely on MAC-derived node IDs in virtualized/containerized environments at all — use the RFC-sanctioned random node ID fallback seeded from a CSPRNG (see Category 1), or better, move to UUIDv7, which has no node-ID concept and re-randomizes on every call.

Guid orderId = Guid.CreateVersion7(); // no node ID, no cloning risk

Category 4: Deterministic UUIDs (v3/v5) Used Where Uniqueness Was Expected

This is the most common actual root cause behind "impossible" duplicate-UUID bug reports, and it isn't a collision at all — it's the generator working exactly as designed. UUIDv3/v5 are deterministic: the same namespace and name always produce the same UUID.

// This isn't a bug — it's the entire point of v5
var ns = new Guid("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
Guid a = GuidUtility.Create(ns, "user:12345"); // same every time
Guid b = GuidUtility.Create(ns, "user:12345"); // == a, always

The failure pattern: a developer generates a v5 UUID from some "unique-looking" input — a filename, an email, a row's natural key — without realizing the same input can legitimately recur (a reprocessed file, a re-imported record, a retried request). The resulting "duplicate ID" report is accurate: the system produced the same UUID twice, because it was told to.

The fix isn't a stronger RNG — it's recognizing whether you actually wanted determinism (v5, keep it) or uniqueness (v4/v7, switch to it):

-- If determinism is genuinely wanted, defend against the reuse explicitly
-- rather than being surprised by it:
ALTER TABLE Documents ADD CONSTRAINT UQ_Documents_ContentHash UNIQUE (Id);
-- ...and handle the constraint violation as "already processed", not as an error.

Category 5: Truncation and Serialization Bugs

A UUID collision can be manufactured entirely outside the generator, by something downstream mangling the value. Common patterns:

  • Storing a UUID in a column too narrow for its full text form (VARCHAR(32) silently truncating a 36-character hyphenated UUID)
  • A hashing or shortening step applied to "make IDs nicer" that reduces effective entropy (e.g., taking the first 8 characters of a UUID string for a "short ID" and never verifying uniqueness of the shortened form)
  • Endianness/byte-order mismatches between two systems' binary UUID representations turning two different UUIDs into the same misread value (covered in depth in our real-world bugs post)
-- This silently truncates and is a common source of "duplicate" IDs
-- that are actually different UUIDs mangled into the same short string
CREATE TABLE BadExample (
    Id VARCHAR(20) PRIMARY KEY -- too narrow for a 36-char UUID
);
-- Always size the column to the actual representation, or better,
-- use the native binary/GUID type so truncation isn't possible at all
CREATE TABLE GoodExample (
    Id UNIQUEIDENTIFIER PRIMARY KEY
);

Category 6: Test Fixtures and Seeded RNGs Leaking Into Production

Deterministic tests often seed their random number generator for reproducibility:

// Common in test suites — intentional, and fine, as long as it stays in tests
const seededRandom = mulberry32(42); // fixed seed -> reproducible "random" UUIDs

The failure pattern is a seeded/mocked ID generator accidentally shipping into a production code path — a stub left in a shared utility module, a feature flag defaulting the wrong way, or a "demo mode" generator reused for real accounts. Every ID generated through that path is not just likely to collide — it's guaranteed to, because the sequence is fully deterministic and typically restarts from the same seed on every process start.

The fix is process, not code: keep test/mock ID generators in test-only modules that can't be imported from production code paths, and add a runtime assertion or lint rule that flags any non-CSPRNG random source used for ID generation outside of test directories.


Comparison: Collision Failure Modes Across ID Schemes

Scheme True Random-Collision Risk Dominant Real-World Failure Mode
UUIDv4 Negligible Weak RNG substitution, truncation, deterministic-generator misuse
UUIDv1/v6 Negligible (with proper node ID) Cloned node/MAC IDs, clock rollback
UUIDv3/v5 N/A — deterministic by design Unexpected input reuse (not a "collision" — expected behavior)
UUIDv7 Negligible Same RNG/truncation risks as v4, plus clock-based ordering assumptions
NanoID (default) Negligible (126 bits) Same RNG-substitution risk; custom short alphabets increase real risk if under-sized
Snowflake/Instagram-style (64-bit) Structurally prevented (not random) Misconfigured/duplicate worker or shard ID assignment

The rightmost column is the important one: for every scheme, the practical collision risk in production is dominated by an implementation or operational failure, not by the underlying probability space being too small. Structured schemes like Snowflake sidestep the randomness question entirely by making uniqueness a property of coordinated identity assignment — which trades "hope the RNG is good" for "hope the worker ID registry is correct," a different but equally real operational responsibility.


Defense in Depth: Assume the Math, Verify the Implementation

Given that real collisions trace back to implementation bugs rather than probability, the practical defense isn't "worry about the odds" — it's standard engineering hygiene applied specifically to ID generation:

-- Always let the database enforce uniqueness as a backstop,
-- regardless of how confident you are in the generator
ALTER TABLE Orders ADD CONSTRAINT UQ_Orders_Id UNIQUE (Id);
// Handle the constraint violation as a real (if rare) event, not
// something that "can't happen" and therefore goes unhandled
try
{
    await _db.SaveChangesAsync();
}
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
{
    // Regenerate and retry, and — importantly — log it. A constraint
    // violation on a UUID primary key is a strong signal something
    // upstream (RNG, truncation, deterministic misuse) is broken,
    // not routine bad luck.
    _logger.LogWarning("UUID collision detected — investigate ID generation path");
}
  1. Use CSPRNG-backed, platform-native generators — never hand-roll, never substitute a general-purpose PRNG.
  2. Keep a real uniqueness constraint at the database level, even though you "shouldn't" need it — it's your only reliable signal that something upstream is broken.
  3. Alert on constraint violations involving ID columns specifically — treat them as a diagnostic signal pointing at a systemic bug, not as routine noise to swallow and retry silently.
  4. Know which version you're actually using, and whether determinism or randomness is what the code path requires — most "collision" reports turn out to be v3/v5 behaving correctly against unexpected input.

The probability math behind UUIDs is sound and doesn't need defending. What breaks in practice is always a layer below the math: the randomness source, the storage format, or a mismatch between what the code expected and what a deterministic algorithm actually guarantees.