What is a UUID Collision Probability
The intuition “one collision every 2122 values” is wrong, and it is wrong in your favour. Because a collision is a match between any two values, the risk grows with the square of how many you have generated. The standard approximation is p ≈ n² / (2 × 2122), which is why the usual headline number is that you need about 2.7 × 1018 values — some three trillion per second for a year — before the chance of a duplicate reaches 50%.
At realistic volumes the numbers are reassuring: a billion v4 UUIDs give a probability of roughly 9 × 10-20. Two more things matter in practice, though. First, the entropy is only as good as the generator: a UUID built on Math.random(), on a broken PRNG or with a fixed seed has far fewer effective bits than the format suggests. Second, the maths assumes independence — if two services generate values from the same seeded source, the birthday bound does not apply to them.
Short identifiers are where this becomes a real decision. The same maths with 64 bits instead of 122 means a billion values already carry about a 2.7% chance of a collision. That is not a reason to avoid compact ids; it is a reason to size them to the job, and to keep a uniqueness constraint in the database as the final backstop.
How to
- Count how many identifiers you will ever need, including historic rows and imports from other systems.
- Read the table below for the corresponding probability.
- If a collision would be costly, keep a UNIQUE constraint — the maths reduces the odds, it never removes the need for one.
Use cases
- Choosing between a UUID and a short id, where 64 bits may be plenty for a cache and far too little for a ledger.
- Reviewing a codebase that generates identifiers itself, to check the entropy source rather than the format.
- Explaining to stakeholders why “random” does not mean “no constraint needed”.
Compare UUID Collision Probability with other formats
| Option | When to use |
|---|---|
10<sup>6</sup> values | ≈ 9.4 × 10<sup>-26</sup> chance of any collision — effectively zero. |
10<sup>9</sup> values | ≈ 9.4 × 10<sup>-20</sup>. |
10<sup>12</sup> values | ≈ 9.4 × 10<sup>-14</sup>. |
10<sup>15</sup> values | ≈ 9.4 × 10<sup>-8</sup> — about one in ten million. |
2.7 × 10<sup>18</sup> values | ≈ 50% — the point where a coin flip would be as good. |
64-bit ids, 10<sup>9</sup> values | ≈ 2.7% — the same maths, but with 58 fewer bits. |
Code examples
Collision probability in Python
from math import exp
N = 2 ** 122 # distinct v4 UUIDs
def collision_probability(n, bits=122):
N = 2 ** bits
return 1 - exp(-(n * n) / (2 * N))
for n in (10**6, 10**9, 10**12, 10**15, 2.7 * 10**18):
print(f'{n:.0e}', collision_probability(n))
# 64-bit ids reach 2.7% at one billion values
print(collision_probability(10**9, bits=64))
Same thing in one line (JavaScript)
const p = (n, bits = 122) => 1 - Math.exp(-(n * n) / (2 * 2 ** bits));
p(1e9); // ~9.4e-20
p(1e9, 64); // ~0.027
Backstop in the schema
CREATE TABLE events (
id uuid PRIMARY KEY, -- or UNIQUE NOT NULL for a non-primary key
created_at timestamptz NOT NULL DEFAULT now()
);
-- The maths makes collisions improbable; the constraint makes them impossible to keep.
Frequently asked questions
Has a UUID v4 collision ever actually happened?
Not by accident at any meaningful scale — the odds are too small for a random generator to be the cause. Almost every reported duplicate turns out to come from a truncated field, a fixed or reused seed, a copy-paste in test data, or a generator that was not random at all (a timestamp with low resolution, for instance).
Should I still add a UNIQUE constraint?
Yes. The probability argument says a collision is unlikely; a constraint says what the database does if one ever occurs. It costs one index and turns a silent data-quality bug into a visible error — which is the outcome you want.
How do the odds change if I shorten the id?
They grow with the square of the volume and shrink exponentially with the bit count. Cutting 128 bits to 64 leaves a billion values with a 2.7% chance instead of 9 × 10-20 — still fine for a cache key with a short lifetime, and far too weak for an identifier a customer will keep for years.