Do UUIDs Collide? Implementation and Operational Patterns That Invite Duplicates
· Updated: · Go Komura · UUID, Identifiers, Distributed Systems, Data Design, Implementation
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614572)
- First published
Cite this article(DOI: 10.5281/zenodo.21614571)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Do UUIDs Collide? Implementation and Operational Patterns That Invite Duplicates. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614571 https://comcomponent.com/en/blog/2026/03/24/001-uuid-collision-bad-implementation-patterns/
- DOI (latest version)
- 10.5281/zenodo.21614571
- DOI (this version)
- 10.5281/zenodo.22218889
You used UUIDs as primary keys, and one day duplicate key shows up.
At that moment, there is a good chance someone says, “So UUIDs do collide after all.”
In practice, however, most UUID duplicates are less a problem with the UUID specification itself than a case where the implementation or operations break the generation conditions the spec assumes. Under RFC 9562, UUIDv4 has 122 bits of random space, and UUIDv7 is also defined on the assumption that the 74 bits outside the timestamp are used for the randomness or counters that provide uniqueness. UUIDv8, on the other hand, is explicitly described as implementation-specific, with uniqueness that must not be assumed.123
The Python standard library likewise documents that uuid4() is generated in a cryptographically secure way, so at least as long as you use a proper implementation in the normal way, the guarantees on the UUID side are quite strong.4
flowchart TB
accTitle: How to read a UUID duplicate failure
accDescr: A diagram showing that most UUID duplicates seen in practice are not a problem with the UUID specification itself but cases where the implementation or operations break the generation conditions the spec assumes, and that the guarantees on the UUID side are quite strong as long as a proper implementation is used in the normal way.
u1["The UUID specification itself"] -.-> u2["Guarantees are strong (122 bits of randomness and so on)"]
u3["Implementation and operations"] --> u4["Break the generation conditions the spec assumes"]
u4 --> u5["Most real duplicate failures land here"]
Figure 1: What to suspect is not the math of UUIDs but the implementation and operations that broke its assumptions.
This article organizes the typical patterns where incorrect operations or implementations make UUIDs collide, together with the measures that prevent recurrence. The content is based on RFC 9562, the official Python documentation, and the official PostgreSQL documentation as verifiable as of March 2026.546
Terms Used in This Article
Several terms appear untranslated in the chapters below, so here is a one-line gloss for each up front.
| Term | What it means in one line |
|---|---|
| PRNG | Pseudo-random number generator. It produces a fixed sequence from a seed, so the same seed reproduces the same sequence |
| CSPRNG | Cryptographically secure PRNG. A generator whose design goals include making the next value hard to predict |
| generator state | The state of the generator: the internal random state, the clock sequence, counters, and anything else that determines the next UUID |
| carefully seeded counter | A counter initialized with care. In UUIDv7 it refers to the field used to produce a sequence within the same millisecond |
| clock rollback | The clock moving backwards. NTP correction or a manual change returns the system clock to an earlier value |
| counter rollover | Counter overflow. The counter passes its maximum value and wraps back to 0 |
| monotonicity | The property that a UUID created later always has a larger value |
| namespace | In UUIDv3 / v5, the UUID that fixes the context in which a name is interpreted |
| canonicalization | Normalization. Reducing the strings that point to the same subject to a single form, down to letter case and trailing slashes |
1. The Conclusion First
To summarize up front, these are the dangerous patterns.
| Pattern | What happens | First countermeasure |
|---|---|---|
| Hand-rolling UUIDv4-like values with a fixed seed or weak PRNG | The same sequence is reproduced in another process or node | Use the OS / runtime standard UUID API |
| Carrying over generator state as is after fork, VM snapshot, or container cloning | Random or counter state rewinds and duplicates appear | Re-seed after fork, re-initialize after clone, review how persistent state is handled |
| Using UUIDv3 / v5 under the misconception that they yield “a new ID every time” | The same UUID is regenerated from the same namespace and same name | Understand they are deterministic IDs and restrict their use |
| Implementing UUIDv1 / v6 / v7 / v8 yourself and handling clock rollback or node/counter carelessly | Duplicates become likely under high-frequency generation or across multiple nodes | Use existing libraries and reduce custom generators |
| Truncating UUIDs midway or squashing them into another format | You throw away the original 128-bit uniqueness yourself | Store and compare at full length |
| Not placing UNIQUE / PRIMARY KEY on the DB side | Duplicates slip in silently and root-cause analysis is delayed | Keep a uniqueness constraint at the storage layer |
In short, rather than the UUID colliding, it is usually that the uniqueness you expected from the UUID was shaved away somewhere in the design.
flowchart TB
accTitle: Where uniqueness gets shaved away
accDescr: A diagram showing that rather than the UUID colliding, the uniqueness expected from a UUID is usually shaved away somewhere in the design, by how it is generated, by duplication and rewinding in operations, by truncation, and by the absence of a constraint.
s0["Uniqueness expected from a UUID"] --> s1["Shaved away in generation (hand-rolled, weak randomness)"]
s0 --> s2["Shaved away in operations (snapshot, fork)"]
s1 --> s3["Shaved away in storage (truncation, no constraint)"]
s2 --> s3
s3 --> s4["Duplicates show up as the result"]
Figure 2: Collisions rarely just happen; they are usually manufactured somewhere in the design.
Knowledge map for this article
This article explains that most UUID collision incidents come not from the standard itself but from generation and operation breaking the assumptions the standard relies on. UUIDv4 as defined by RFC 9562 assumes 122 bits of randomness produced by a CSPRNG, and UUIDv7 assumes a design in which a counter provides monotonicity, so a hand-rolled generator built on a weak PRNG with a fixed seed, a generator state rolled back after a fork or a snapshot, misuse of UUIDv3/v5 for issuing new identifiers, overconfidence in the implementation-dependent uniqueness of UUIDv8, and truncation that cuts into the 128 bits all lead to duplicates. Because the RFC itself states that it does not absolutely guarantee true global uniqueness, a UNIQUE constraint on the database side is the last line of defense that prevents a duplicate from being inserted.
flowchart LR
accTitle: Implementation patterns that cause UUID collisions
accDescr: Diagram showing that UUIDv4, UUIDv7, UUIDv3/v5, UUIDv8, and UUIDv1/v6 are each defined in RFC 9562, that a hand-rolled generator built on a weak PRNG, a rolled-back generator state, misuse of a name-based UUID for issuing new identifiers, counter rollover, clock rollback, and truncation all cause UUID collisions, and that using a CSPRNG, reseeding after a fork, and a uniqueness constraint on the database side are the countermeasures
uuid["UUID"]
rfc9562["RFC 9562 (UUID Standard)"]
csprng["CSPRNG"]
uuidv4["UUIDv4 (Random-Based)"]
weak_prng_uuid["Hand-Rolled UUID from Weak PRNG"]
new_record_id_issuance["New Record ID Assignment"]
name_based_uuid["Name-Based UUID (v3/v5)"]
uuidv8["UUIDv8 (Experimental/Vendor-Specific)"]
uuid_collision["UUID Collision"]
generator_state["Generator State"]
counter_rollover["Counter Rollover"]
clock_rollback["Clock Rollback"]
uuid_truncation["UUID Truncation"]
db_unique_constraint["Database Unique Constraint"]
fork_reseed["Reseeding After fork"]
uuidv7["UUIDv7 (Time-Ordered)"]
monotonicity["Monotonicity"]
namespace_uuid["Namespace UUID"]
canonicalization["Canonicalization"]
uuidv1_v6["UUIDv1/v6 (Time-Based)"]
csprng -->|"recommended for"| uuidv4
weak_prng_uuid -->|"not recommended for"| new_record_id_issuance
name_based_uuid -->|"not recommended for"| new_record_id_issuance
uuidv8 -->|"not recommended for"| new_record_id_issuance
weak_prng_uuid -.->|"may cause"| uuid_collision
generator_state -.->|"may cause"| uuid_collision
name_based_uuid -.->|"may cause"| uuid_collision
counter_rollover -.->|"may cause"| uuid_collision
clock_rollback -.->|"may cause"| uuid_collision
uuid_truncation -->|"may cause"| uuid_collision
db_unique_constraint -->|"mitigates"| uuid_collision
fork_reseed -->|"mitigates"| uuid_collision
csprng -->|"prevents"| weak_prng_uuid
uuidv7 -.->|"uses"| monotonicity
counter_rollover -.->|"incompatible with"| monotonicity
clock_rollback -.->|"incompatible with"| monotonicity
name_based_uuid -->|"uses"| namespace_uuid
name_based_uuid -->|"requires"| canonicalization
uuidv4 -->|"verified by"| rfc9562
uuidv7 -->|"verified by"| rfc9562
name_based_uuid -->|"verified by"| rfc9562
uuidv8 -->|"verified by"| rfc9562
uuidv1_v6 -->|"verified by"| rfc9562
uuidv1_v6 -.->|"may cause"| uuid_collision
uuid -->|"verified by"| rfc9562
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (25 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Suspect the Generation and Operations First, Not the Math of UUIDs
UUID discussions get confusing because the properties differ by version.
- UUIDv4 is random-based. Under RFC 9562, the 122 bits other than version / variant are filled with random data (Section 5.4).1
- UUIDv7 has a structure that sorts well chronologically: on top of a Unix millisecond timestamp, the rest is composed of randomness or a carefully seeded counter (Section 5.7).2
- UUIDv3 / v5 are name-based. Given the same namespace and the same canonical name, producing the same UUID is the correct behavior (Section 6.5).7
- UUIDv8 is for experimental and vendor-specific use, and its uniqueness is implementation-specific. The RFC says uniqueness must not be assumed (Section 5.8).3
flowchart TB
accTitle: Properties differ by version
accDescr: A diagram showing that the properties differ completely by version: UUIDv4 is random-based, UUIDv7 combines a timestamp with randomness or a counter into a time-ordered structure, UUIDv3 and v5 are name-based and return the same value for the same input, and the uniqueness of UUIDv8 is implementation-specific.
v0["UUID version"] --> v1["v4: 122 bits of randomness"]
v0 --> v2["v7: time + randomness or counter"]
v1 --> v3["v3 / v5: same input, same value (name-based)"]
v2 --> v4["v8: uniqueness is implementation-specific"]
Figure 3: Saying you use UUIDs settles nothing; the version changes the properties completely.
So even if you say “we use UUIDs,” the story changes completely depending on whether what is inside is
- the standard library’s
uuid4() - a homemade
timestamp + random uuid5(namespace, name)- or a custom format that merely looks like UUIDv8.
flowchart TD
A[UUID duplicate is found] --> B{Where did the same value really come from}
B --> C[Weak generator]
B --> D[State was rewound]
B --> E[Misuse of name-based UUID]
B --> F[Truncated at storage time]
B --> G[No uniqueness constraint on the DB side]
C --> H[Implementation mistake]
D --> H
E --> H
F --> H
G --> H
Figure 4: The causes of a duplicate narrow down to five families: the generator, state, misuse, truncation, and no constraint.
In practice, working from the right side of this diagram is faster.
Written out as a procedure, the practical approach is to clear the cheapest checks first. In the order below, the tools you need grow step by step – run one SQL query, grep the code, read the operational runbook – so you can stop as soon as the cause turns up.
flowchart TD
S["A duplicate key error or duplicate data is detected"] --> Q1{"1. Is there a UNIQUE / PRIMARY KEY in the DB (Chapter 8)"}
Q1 -- No --> A1["Duplicates slipped in silently with no constraint.<br/>Add the constraint first and close the entry point"]
Q1 -- Yes --> Q2{"2. Are the full 128 bits kept in storage and comparison (Chapter 7)"}
Q2 -- Truncated --> A2["Suspect prefix comparison, squashing into 64 bits,<br/>or a tail cut off by an undersized column"]
Q2 -- Kept --> Q3{"3. Is generation done through a standard API (Chapters 3 and 5)"}
Q3 -- Hand-rolled --> A3["Suspect hand-rolling with a weak PRNG, or<br/>misuse of a name-based UUID for ID assignment"]
Q3 -- Standard API --> Q4{"4. Is generator state carried over after fork / snapshot / clone (Chapter 4)"}
Q4 -- Carried over --> A4["The generator state has been rewound"]
Q4 -- No problem --> A5["What remains is a custom time-based implementation<br/>or the design of UUIDv8 itself (Chapter 6)"]
Figure 5: A diagnostic flow that works from cheapest to most expensive: one SQL query, then grep, then a look at the operational procedure.
3. Pattern 1: Calling It UUIDv4 While Actually Using a Weak PRNG
This is the most common one.
- Building 128 bits with a general-purpose PRNG on the level of
Math.random() - Seeding at startup with
time()or the PID - Hand-assembling “32 hex digits that look like UUID format”
It may look like a UUID, but if the random source is weak, the same sequence gets reproduced in another process or on another node.
It is easier to see in code. The following runs on nothing but the Python 3 standard library, and it is the version you must not write.
# Bad example: run a general-purpose PRNG with a fixed seed and hand-assemble a UUID-shaped string
import random
def make_pseudo_uuid(seed: int) -> str:
rng = random.Random(seed) # the same seed produces exactly the same sequence every time
value = rng.getrandbits(128)
hex_digits = f"{value:032x}"
return "-".join([
hex_digits[0:8],
hex_digits[8:12],
hex_digits[12:16],
hex_digits[16:20],
hex_digits[20:32],
])
first = make_pseudo_uuid(12345)
second = make_pseudo_uuid(12345)
print(first == second) # True: same seed, same value, in another process or on another node
There are in fact two problems with this example.
random.Randomis a general-purpose PRNG, so an identical seed reproduces the sequence exactly. Seeding from the startup time or the PID does not save you either: simultaneous startups or clones can still collide.- It never sets the version / variant bits, so it is not an RFC 9562 UUIDv4 in the first place. To be accurate, it is nothing more than a 128-bit number shaped like a UUID.
flowchart TB
accTitle: Two problems with a hand-rolled pseudo UUID
accDescr: A diagram showing that running a general-purpose PRNG with a fixed seed to assemble a UUID-shaped string reproduces the same sequence for the same seed and yields identical values in another process or on another node, and that because the version and variant bits are never set it is not even an RFC 9562 UUIDv4 but only a 128-bit number shaped like a UUID.
b1["Hand-rolled with a general-purpose PRNG and a fixed seed"] --> b2["The same seed reproduces the same sequence"]
b1 --> b3["The version / variant bits are never set"]
b2 --> b4["The same value appears in another process or node"]
b3 --> b5["Only a 128-bit number shaped like a UUID"]
Figure 6: The bad example fails on two counts, reproducibility and bit setting, and is not a UUIDv4 at all.
By contrast, the good version is almost anticlimactically short.
# Good example: use the standard API as is. Do not touch the seed or the version yourself
import uuid
new_id = uuid.uuid4()
print(new_id.version) # 4: the API fills in the version bits correctly
print(uuid.uuid4() != uuid.uuid4()) # True: every call returns a different value
RFC 9562 says a CSPRNG should be used, both for UUID uniqueness and for unguessability (Section 6.9 Unguessability). This is a recommendation (SHOULD), so some use cases can justify an exception, but if you hand-roll UUIDs with a general-purpose PRNG you should be able to explain why. It goes on to say that the CSPRNG state should be re-seeded appropriately on state changes such as a process fork.8
Python’s uuid.uuid4() is likewise documented as generating random UUIDs in a cryptographically secure way.4
The practical conclusion here is simple.
- Do not hand-roll UUIDs
- Do not fiddle with random seeds by hand
- Use the standard library, or a widely used implementation, as is
Keeping a custom generator around “because it is lightweight” or “because we have always used it” is what ends up costing the most later.
flowchart TB
accTitle: Practical conclusions about generation
accDescr: A diagram showing that RFC 9562 says a CSPRNG should be used for uniqueness and unguessability and also covers re-seeding on fork, and that the practical conclusion comes down to three points: do not hand-roll UUIDs, do not fiddle with random seeds by hand, and use the standard library or a widely used implementation as is.
r1["Do not hand-roll UUIDs"] --> r2["Do not fiddle with random seeds by hand"]
r2 --> r3["Use the standard library or a widely used implementation"]
r3 -.-> r4["Using a CSPRNG and re-seeding after fork are RFC recommendations"]
Figure 7: The conclusion about generation is simple: do not hand-roll, lean on the standard API.
4. Pattern 2: Rewinding Generator State with fork, Snapshot, or Clone
The second most dangerous item is operations in which the generator state gets duplicated or rewound.
RFC 9562 explicitly recommends re-seeding after a fork (Section 6.9), and it explains that an implementation without stable storage has to generate clock sequences, counters, and random data more often, which raises the probability of duplicates (Section 6.3 UUID Generator States).89
A practical line of reasoning follows naturally from that.
- Restoring several copies of the same image after taking a VM snapshot
- A custom generator coming up from the same initial state every time a container image starts
- Sharing PRNG state or counter state across worker forks
Under operations like these, the UUID generation sequence can be reproduced unintentionally. The RFC does not literally say “snapshots are dangerous,” but this is a very practical caution you can derive from its notes on re-seeding after fork and on handling generator state.89
flowchart TB
accTitle: How duplicated or rewound state produces duplicates
accDescr: A diagram showing that when a VM snapshot restore, a container image copy, or a worker fork carries the generator state over unchanged, the random and counter state rewinds, the generation sequence is reproduced unintentionally and duplicates appear, and that the countermeasure is to re-initialize immediately after fork, clone, or restore.
c1["Snapshot restore, container copy, worker fork"] --> c2["Generator state is carried over unchanged"]
c2 --> c3["Random and counter state rewinds"]
c3 --> c4["The generation sequence repeats and duplicates appear"]
c2 -.-> c5["Fix: re-initialize immediately and lean on OS randomness"]
Figure 8: Duplicating or rewinding a machine duplicates its generation sequence along with it.
Countermeasures look roughly like this.
- Do not hold custom UUID generator state for long
- Re-initialize immediately after fork / clone / restore
- Where possible, move to an implementation that draws OS-provided randomness on every call
- For a high-frequency generator, write the state management and re-seeding rules into the specification
5. Pattern 3: Misreading UUIDv3 / v5 as “a New ID Every Time”
UUIDv3 / v5 are not random IDs that resist collision. They are deterministic IDs that regenerate the same ID from the same name.
RFC 9562 states that UUIDs generated from the same name in canonical format within the same namespace must be equal (Section 6.5 Name-Based UUID Generation).7 So with usage like the following, a duplicate is not a failure – it is the specified behavior.
flowchart TB
accTitle: How to read name-based UUIDs correctly
accDescr: A diagram showing that UUIDv3 and v5 are not collision-resistant random IDs but deterministic IDs that regenerate the same UUID from the same namespace and the same canonical name, so a duplicate produced by using them for fresh ID assignment is not a failure but the specified behavior.
n1["Same namespace + same canonical name"] --> n2["Always the same UUID (a MUST in the spec)"]
n2 --> n3["Used for fresh ID assignment, duplicates are per spec"]
n1 -.-> n4["A deterministic ID, not a random ID"]
Figure 9: A v3 / v5 duplicate is not a failure; it is the deterministic-ID specification working as written.
- Using
uuid5(NAMESPACE_URL, "https://example.com/users/42")as “fresh ID assignment” every time - Issuing IDs from one namespace shared across all customers plus an email address, without putting the tenant into the namespace
- Assuming that re-issuing the same logical name on each retry produces a different ID
A short piece of code makes this visible too. It runs on the Python 3 standard library alone.
# Misuse example: using uuid5 as if it were fresh ID assignment
import uuid
url = "https://example.com/users/42"
first = uuid.uuid5(uuid.NAMESPACE_URL, url)
second = uuid.uuid5(uuid.NAMESPACE_URL, url)
print(first == second) # True: the same value however many times you call it. This is the specified behavior
# And when the normalization of the name drifts, you get the opposite problem: a different ID
with_slash = uuid.uuid5(uuid.NAMESPACE_URL, url + "/")
print(first == with_slash) # False: one trailing slash makes it a different UUID
# If you want fresh ID assignment, you need a different version to begin with
print(uuid.uuid4() != uuid.uuid4()) # True
If you do use uuid5, the safe approach is to settle how the name is normalized before it is passed in and to keep that normalization function in one place.
# Good example: factor canonicalization into a function and always pass through it before calling uuid5
import uuid
# Give each tenant its own namespace. Fix this value in the specification and never change it later
TENANT_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "tenant-a.example.com")
def canonical_user_url(user_id: int) -> str:
# Settle on one form, down to the scheme, the host, and whether a trailing slash is present
return f"https://example.com/users/{user_id}"
def user_uuid(user_id: int) -> uuid.UUID:
return uuid.uuid5(TENANT_NAMESPACE, canonical_user_url(user_id))
print(user_uuid(42) == user_uuid(42)) # True: the same subject always gets the same ID
Conversely, when the canonicalization of the name drifts, you get different UUIDs for the same subject. The RFC stresses the handling of the canonical representation repeatedly (Section 5.5, Section 6.5).710
Three things matter in this family:
- UUIDv3 / v5 are not “ID assignment without duplicates” but “same input, same ID”
- Do not leave the namespace design vague
- Write the canonicalization of names into the specification
flowchart TB
accTitle: What drifting canonicalization causes
accDescr: A diagram showing that when the normalization of a name drifts the same subject gets a different UUID while the same input always gives the same ID, so the safe approach is to avoid a vague namespace design and to keep the normalization function in one place and always pass through it before calling uuid5.
m1["Normalization of the name"] --> m2{"Does it drift"}
m2 -->|"Keep one normalization function and always use it"| m3["The same subject always gets the same ID"]
m2 -.->|"A trailing slash and the like drift"| m4["The same subject gets a different UUID"]
m1 -.-> m5["Fix the namespace design in the specification too"]
Figure 10: Without a specified namespace and canonicalization, you also get the reverse failure: one subject, two IDs.
6. Pattern 4: Hand-Implementing Time-Based UUIDs or UUIDv8
UUIDv1 / v6 / v7 / v8 are dangerous to copy by appearance alone.
6.1 Handling node or clock sequence carelessly in UUIDv1 / v6
Under RFC 9562, UUIDv6 is UUIDv1 with the fields reordered to improve DB locality, and it deals with clock sequences and nodes (Section 5.6). The RFC also carries a number of cautions about node collision resistance in distributed environments (Section 6.4) and about keeping state (Section 6.3).11912
It goes further and says that with the arrival of virtual machines and containers, the uniqueness of MAC addresses can no longer be guaranteed.5
So designs like
- assuming “it is a MAC address, so it must be unique”
- replicating a node ID baked into an image
- resetting the clock sequence to a fixed value on every restart
are dangerous.
flowchart TB
accTitle: Dangerous assumptions in UUIDv1 / v6
accDescr: A diagram showing that the RFC states the uniqueness of MAC addresses can no longer be guaranteed now that virtual machines and containers exist, so designs that assume a MAC address is unique, that replicate a node ID baked into an image, or that reset the clock sequence to a fixed value on every restart are dangerous.
d1["Assume a MAC address is unique"] -.-> d4["Dangerous design"]
d2["Replicate a node ID baked into an image"] -.-> d4
d3["Reset the clock sequence to a fixed value"] -.-> d4
d4 --> d5["In virtualized environments MAC uniqueness is not guaranteed"]
Figure 11: MAC uniqueness, the assumption behind v1 / v6, does not hold in the age of virtualization.
6.2 Hand-rolling UUIDv7 and leaving counter rollover or clock rollback unhandled
UUIDv7 is quite practical, but the RFC treats monotonicity and counter handling under high-frequency generation carefully (Section 6.2 Monotonicity and Counters). It also states explicitly that an implementation must not knowingly return a duplicate on clock rollback or counter rollover.213
Which means implementations like
- issuing large volumes within the same millisecond with no counter design
- carrying on generating without doing anything when the clock moves backwards
- multiple processes each initializing the same internal counter independently
are risky.
flowchart TB
accTitle: What is risky to leave unhandled in a hand-rolled UUIDv7
accDescr: A diagram showing that issuing large volumes within the same millisecond with no counter design, carrying on generating when the clock moves backwards, and multiple processes initializing the same internal counter independently are risky implementations, and that the RFC forbids knowingly returning a value that is a duplicate on clock rollback or counter rollover.
e1["High-volume issuance with no counter design"] -.-> e4["A hand-rolled v7 prone to duplicates"]
e2["Clock rollback left unhandled"] -.-> e4
e3["The same counter initialized independently"] -.-> e4
e4 --> e5["Knowingly returning a duplicate is forbidden"]
Figure 12: In a hand-rolled v7, how you handle the counter and clock rollback is the whole game.
At What Issuance Rate Should You Start Worrying
To judge whether your own system is affected, the quickest route is to look at the bit allocation.
UUIDv7 in RFC 9562 has a 48-bit millisecond timestamp, followed by rand_a (12 bits), and then, after the variant, rand_b (62 bits).2
Section 6.2 then presents ways to preserve monotonicity: dedicating the 12 bits of rand_a to a counter (Method 1), using the rand_b side as a “randomly initialized counter” (Method 2), and replacing up to 12 bits of rand_a with sub-millisecond timestamp precision (Method 3).13
From that, the rough guide reads as follows.
Here, do not simply read 12 bits as 4096 available values. RFC 9562 asks that the counter be initialized to a random value on each tick so that it is harder to predict.13 If the initial value is 4000, only 96 values are left for that tick. The number available within one tick is “4096 minus the initial value,” not 4096.
As a countermeasure the RFC offers keeping the high bits of the counter at 0 and initializing only the low side.13 If, for example, you fix the most significant bit to 0 and randomize only the low 11 bits, the initial value is at most 2047, so every tick is guaranteed at least 2048 values. The number you can guarantee is set not by the counter width but by the range allowed for initialization.
With that in mind, the rough guide comes out like this.
| Issuance per generator per millisecond | How to think about it |
|---|---|
| A few to a few dozen | With an initialization that reserves the high bits, you will not exhaust a tick |
| A few hundred | Depending on how you initialize, this is where it wraps. This is the level at which you set an upper bound on the initial value and write the rollover behavior (advance the timestamp / wait) into the specification |
| 1000 or more | Even reserving the high bits leaves little headroom. Assume Method 2, which also uses rand_b, or a design that advances the timestamp |
“We do not issue millions per second, so rollover is irrelevant” does not follow. The ceiling is set by how you initialize, so even a moderate issuance rate can wrap if the initial value is drawn from the full 12-bit range. Ship without deciding what happens on rollover, and that is where duplicates appear. Decide the range allowed for initialization first, then the behavior on rollover, in that order.
flowchart TB
accTitle: What determines the number of values a counter can guarantee
accDescr: A diagram showing that because RFC 9562 asks for the counter to be initialized to a random value on each tick the number available within one tick is 4096 minus the initial value, that reserving the high bits at 0 and randomizing only the low side sets a guaranteed minimum, and that the design order is to decide the range allowed for initialization first and the rollover behavior second.
f1["Initialize to a random value on each tick"] --> f2["Available values are 4096 minus the initial value"]
f2 --> f3["Reserving the high bits guarantees a minimum count"]
f3 --> f4["1. Decide the range allowed for initialization"]
f4 --> f5["2. Decide the behavior on rollover"]
Figure 13: The count you can guarantee comes from the initialization range, not the counter width.
There are two points, though, where it is better not to stop and relax.
- Clock rollback happens regardless of issuance rate. NTP correction, resuming a suspended VM, and manual clock changes all move the clock backwards routinely. A system that creates only ten UUIDs per second still needs a countermeasure.
- The figure is “per generator,” so you have to multiply by the number of processes. If 100 processes each hold their own counter and all start from the same initial value, the sequences overlap even when each process issues only a few IDs.
6.3 Reaching for UUIDv8 as casually as “the new UUID spec”
UUIDv8 looks convenient, but RFC 9562 is quite clear: the uniqueness of UUIDv8 is implementation-specific and must not be assumed (Section 5.8).3
So a “company-proprietary UUID” that
- embeds a timestamp
- embeds a shard ID
- embeds some business meaning
- and fills the rest with whatever randomness
means that design document is itself the uniqueness specification for your UUIDs. It is far too dangerous to introduce without review.
flowchart TB
accTitle: What choosing UUIDv8 means
accDescr: A diagram showing that because RFC 9562 says the uniqueness of UUIDv8 is implementation-specific and must not be assumed, a proprietary UUID that embeds a timestamp, a shard ID, or business meaning makes your own design document the uniqueness specification itself, which is far too dangerous to introduce without review.
g1["Build a proprietary format on UUIDv8"] --> g2["Uniqueness is implementation-specific and the spec guarantees nothing"]
g2 --> g3["Your design document becomes the uniqueness specification"]
g3 --> g4["Introducing it without review is far too dangerous"]
Figure 14: Choosing v8 means taking on the responsibility for uniqueness yourself.
7. Pattern 5: Shortening the UUID Along the Way
Even when generation is correct, things can break at the storage or comparison stage.
Some typical examples:
- Using only the first 8 characters as a stand-in for a foreign key
- Squashing a 128-bit UUID into a 64-bit integer
- A string column that is too short, so the tail gets cut off
- Treating the shortened form used in logs or on screen as the unique key
What matters here is that changing the representation is not the problem in itself.
- Removing hyphens
- Normalizing to lower or upper case
- Storing as 16 binary bytes
Transformations like these, which lose none of the 128 bits, are fine. What is dangerous is a transformation that removes the material of uniqueness itself.
A design where a separate “human-friendly short ID” was created and then quietly started taking precedence over the real UUID is especially prone to going wrong.
flowchart TB
accTitle: A change of representation versus a change that removes uniqueness
accDescr: A diagram showing that transformations which keep all 128 bits such as removing hyphens, normalizing letter case, and storing 16 binary bytes are fine, while transformations that remove the material of uniqueness such as using only the first 8 characters, squashing into a 64-bit integer, and losing the tail to an undersized column are dangerous.
h0{"Does the transformation keep all 128 bits"}
h0 -->|"It keeps them"| h1["Hyphens removed, case normalized, stored as binary"]
h0 -.->|"It removes some"| h2["First 8 characters, 64-bit squash, truncated tail"]
h1 --> h3["A harmless transformation"]
h2 --> h4["A dangerous transformation that throws uniqueness away"]
Figure 15: The problem is not changing the representation; it is removing the material of uniqueness.
8. Pattern 6: No Uniqueness Constraint on the DB Side
And this one is especially important.
Even if UUIDs are sufficiently collision-resistant, if you truly cannot tolerate duplicates, the place they are stored should carry a uniqueness constraint too.
The official PostgreSQL documentation explains that a unique constraint guarantees the value of a column or set of columns is unique across the whole table, and that a primary key is a row identifier that is unique and not null.6
RFC 9562 also says that while UUIDs can provide sufficient uniqueness in practice, true global uniqueness can never be absolutely guaranteed, and that uses where the impact of a collision is high should take stronger countermeasures (Section 6.7 Collision Resistance, Section 6.8 Global and Local Uniqueness).14
In practice, this combination is the baseline.
- Use UUIDs as IDs that are unlikely to collide
- Keep UNIQUE / PRIMARY KEY in the DB as the last line of defense
- Design retry, idempotency, and incident logging for the duplicate case
Using UUIDs and omitting a uniqueness constraint are not the same thing.
flowchart TB
accTitle: The DB uniqueness constraint as the last line of defense
accDescr: A diagram showing that the baseline combination is to use UUIDs as IDs unlikely to collide while recognizing that the RFC itself says true global uniqueness can never be absolutely guaranteed, to keep UNIQUE or PRIMARY KEY in the DB as the last line of defense when duplicates truly cannot be tolerated, and to design retry, idempotency, and incident logging for the duplicate case.
i1["Use UUIDs as IDs unlikely to collide"] --> i2["Even so, no absolute guarantee (stated in the RFC)"]
i2 --> i3["Make UNIQUE / PRIMARY KEY the last line of defense"]
i3 --> i4["Design retry, idempotency, and logging for duplicates"]
Figure 16: Using UUIDs and omitting a uniqueness constraint are not the same thing.
9. A Practical Checklist
Finally, here it is in a form you can lift straight into an adoption review or an audit. Where the conclusion table in Chapter 1 listed what is dangerous, this one lists how to check your own system.
| # | What to check | How to check it | Passing bar |
|---|---|---|---|
| 1 | Whether UUIDs are being generated by hand | grep the whole repository for traces of hand assembly such as getrandbits, Math.random, new Random(, and %032x |
UUID generation goes only through standard APIs such as uuid4() / uuid7() |
| 2 | Whether the UUID version is fixed in the specification | Count versions in the stored data. In PostgreSQL, substring(id::text from 15 for 1) is the version digit |
The permitted versions are documented and the real data matches them |
| 3 | Whether you have taken stock of how seeds and generator state are handled | Read the startup scripts, the Dockerfile, and the snapshot / clone runbooks for any mention of re-initialization. grep for the places where workers are forked | The procedure explicitly recreates the generator right after a fork, worker restart, snapshot, or clone |
| 4 | Whether full length is preserved at storage time | Check the column definitions. Also grep the code for truncation such as [:8], substring(, Left(, and ToString("N").Substring |
Storage and comparison both stay at 128 bits. Shortened forms are limited to display |
| 5 | Whether the DB has a UNIQUE / PRIMARY KEY | Use the SQL below, naming the column that holds the UUID, to list both constraints and unique indexes (counting per table counts the primary key on a sequential id) | For that column, at least one row has single_col set to true |
| 6 | Whether duplicates can be observed | grep for places that swallow the exception equivalent to a duplicate key. Look at real log output to see whether the generator / node / deployment appears | Duplicates are recorded as exceptions and you can trace where they came from |
For check 5, one query is enough in PostgreSQL.
-- List whether uniqueness is enforced on the uuid column of public.orders.
-- Replace the table name and the column name with your own
WITH target AS (
SELECT attrelid, attnum
FROM pg_attribute
WHERE attrelid = 'public.orders'::regclass
AND attname = 'uuid' -- <- the column to inspect
AND NOT attisdropped
)
SELECT c.conname AS name,
c.contype::text AS kind, -- p = primary key / u = unique constraint
array_length(c.conkey, 1) = 1 AS prevents_dup, -- can that column alone prevent duplicates
pg_get_constraintdef(c.oid) AS definition
FROM pg_constraint AS c JOIN target AS t ON c.conrelid = t.attrelid
WHERE c.contype IN ('p', 'u')
AND t.attnum = ANY (c.conkey) -- only the ones with that column in the key
UNION ALL
SELECT i.relname AS name,
'i' AS kind, -- i = unique index with no constraint behind it
-- a partial index (indpred not null) guarantees uniqueness only among the matching rows
x.indnkeyatts = 1 AND x.indpred IS NULL AS prevents_dup,
pg_get_indexdef(x.indexrelid) AS definition
FROM pg_index AS x
JOIN pg_class AS i ON i.oid = x.indexrelid
JOIN target AS t ON x.indrelid = t.attrelid
WHERE x.indisunique
AND EXISTS ( -- INCLUDE columns are not key columns, so do not count them.
SELECT 1 -- look only at the first indnkeyatts entries of indkey
FROM generate_series(0, x.indnkeyatts - 1) AS k(i) -- indkey is 0-based
WHERE x.indkey[k.i] = t.attnum)
AND NOT EXISTS ( -- indexes backing a constraint are already listed above
SELECT 1 FROM pg_constraint AS c2 WHERE c2.conindid = x.indexrelid);
Reading the result takes two steps.
kindispfor a primary key,ufor a unique constraint, andifor a unique index created withCREATE UNIQUE INDEXthat has no constraint behind it.15- If not a single row has
prevents_dupset totrue, duplicates on that column are not being prevented.
flowchart TB
accTitle: The two catalog sources behind the uniqueness check
accDescr: A diagram showing that checking uniqueness means querying both pg_constraint, where primary keys and unique constraints appear, and pg_index, where a CREATE UNIQUE INDEX with no constraint behind it appears, and judging single-column uniqueness with prevents_dup in either form.
chk["Check uniqueness on the UUID column"] --> con["pg_constraint (p and u)"]
chk --> idx["pg_index (unique indexes)"]
con --> pd["Judge single-column uniqueness with prevents_dup"]
idx --> pd
Figure 17: Constraints and unique indexes live in different catalogs, so query both and judge with prevents_dup.
Do not decide this on “does the table have a uniqueness constraint.” A design where the primary key sits on a sequential id while the UUID column carries no constraint at all is completely ordinary. Count per table and you will report that state as “last line of defense present.” For the same reason, a composite key that includes the UUID still lets a duplicate UUID through on its own, so you need to look at prevents_dup.
One more thing: do not look at pg_constraint alone. Designs that add uniqueness with CREATE UNIQUE INDEX are ordinary too, and those appear only in pg_index. Count constraints alone, report “there is no last line of defense,” and you overlook an index that already exists while the conversation moves on to an unnecessary schema change. The single query above catches both forms (indnkeyatts requires PostgreSQL 11 or later).
Writing the index-side test as simply “is that column present in indkey” produces two kinds of false positive. Both fall on the “it is protected” side, so you end up stamping it as passing.
- Counting
INCLUDEcolumns as key columns. InCREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid),indkeyalso listsuuid, which is not part of the key.15 Meanwhileindnkeyattsis the number of key columns (1 in this example), soindnkeyatts = 1and “containsuuid” hold at the same time, and an index built onidends up counted as “preventing duplicateuuidvalues.” The key is only the firstindnkeyattsentries ofindkey, so match against those alone (indkeyis 0-based15) - Counting a partial index as a whole-table guarantee.
CREATE UNIQUE INDEX ... ON orders(uuid) WHERE activeguarantees uniqueness only among the rows that match the condition. The same UUID can coexist quite happily between two rows that are notactive, or between a row in the index and a row outside it. A non-nullindpredmeans the index is partial,15 so do not count it as whole-table duplicate prevention (the index itself stays in the listing, so you can read theWHEREclause indefinitionand treat it as a conditional line of defense)
flowchart TB
accTitle: Two traps that produce false positives in a uniqueness audit
accDescr: A diagram showing that an audit which counts constraints per table falsely reports a sequential primary key as the last line of defense, that looking only at pg_constraint overlooks a unique index created with CREATE UNIQUE INDEX, and that counting INCLUDE columns as key columns and counting a partial index as a whole-table guarantee both fall on the protected side, so the UUID column must be named and prevents_dup examined.
j1["Inspect by naming the UUID column"] --> j2["Pick up both constraints and indexes"]
j2 --> j3["Look at prevents_dup"]
j1 -.-> j4["Per-table counting misreports"]
j2 -.-> j5["False positives from INCLUDE columns and partial indexes"]
Figure 18: An audit that does not name the column and ask whether that column alone is unique will report false positives.
10. Summary
UUID collision failures usually start not because the UUID is weak but because the implementation or operations break the assumptions the UUID relies on.
- Hand-rolling with weak randomness
- Rewinding state after a fork or a snapshot
- Using name-based UUIDs for ID assignment
- Casually hand-implementing v7 or v8
- Shortening the value along the way and discarding uniqueness
- Dropping the uniqueness constraint on the DB side
Do any of these, and it is barely different from actively building the conditions for a collision.
When you find a duplicate, what to suspect first is not the math of UUIDs but the generator, state management, the storage format, and the constraint design. Look in that order and the cause usually narrows down a long way.
flowchart TB
accTitle: The order to suspect things in when you find a duplicate
accDescr: A diagram showing that when you find a duplicate, suspecting the generator, then state management, then the storage format, then the constraint design, rather than the math of UUIDs, usually narrows the cause down a long way.
k0["A duplicate is found"] --> k1["1. Suspect the generator"]
k1 --> k2["2. Suspect state management"]
k2 --> k3["3. Suspect the storage format"]
k3 --> k4["4. Suspect the constraint design"]
k0 -.-> k5["The math of UUIDs can wait until last"]
Figure 19: Look in this order and the cause of a duplicate UUID is nearly always pinned down.
11. Related Articles
- A Practical Guide to FileSystemWatcher - Handling Missed and Duplicate Events
- Mutual Exclusion Fundamentals for File-Based Integration - Best Practices for File Locks and Atomic Claims
12. References
-
IETF RFC 9562, Section 5.4 UUID Version 4. On the 122-bit random space of UUIDv4. ↩ ↩2
-
IETF RFC 9562, Section 5.7 UUID Version 7. On the design of UUIDv7’s timestamp, random bits, and counter. ↩ ↩2 ↩3 ↩4
-
IETF RFC 9562, Section 5.8 UUID Version 8. On UUIDv8 uniqueness being implementation-specific and not to be assumed. ↩ ↩2 ↩3
-
Python 3.14 documentation,
uuidmodule. Onuuid4()’s cryptographically-secure generation,uuid5()’s deterministic behavior, and the properties ofuuid7()/uuid8(). ↩ ↩2 ↩3 -
IETF RFC 9562, Universally Unique IDentifiers (UUIDs). The baseline document for the UUID format, each version, and best practices overall. ↩ ↩2
-
PostgreSQL documentation, Constraints. On guaranteeing uniqueness via UNIQUE constraints and PRIMARY KEY. ↩ ↩2
-
IETF RFC 9562, Section 6.5 Name-Based UUID Generation. On the same namespace + same name yielding the same UUID, and the importance of canonicalization. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.9 Unguessability. On CSPRNG use and re-seeding after fork. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.3 UUID Generator States. On handling stable storage and generator state. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.5 UUID Version 5. On the specification of name-based UUIDs built from a namespace plus a canonical name. ↩
-
IETF RFC 9562, Section 5.6 UUID Version 6. On UUIDv6’s node / clock sequence / DB locality. ↩
-
IETF RFC 9562, Section 6.4 Distributed UUID Generation. On node collision resistance in distributed environments. ↩
-
IETF RFC 9562, Section 6.2 Monotonicity and Counters. On cautions around clock rollback, counter rollover, and batch generation. ↩ ↩2 ↩3 ↩4
-
IETF RFC 9562, Sections 6.7 and 6.8. On the thinking behind collision resistance and global uniqueness. ↩
-
PostgreSQL documentation, pg_constraint. On
contypebeingpfor a primary key andufor a unique constraint,conrelidpointing at the table the constraint applies to, andconindidpointing at the index that backs the constraint. A unique index with no constraint behind it appears only on the pg_index side (indrelidis the target table,indisuniquesays whether it is unique). ↩ ↩2 ↩3 ↩4
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
The Depths of Windows Virtualization (Part 3) — Virtual Machines That Boot in Seconds: Why WSL2, Windows Sandbox, and Containers Are So Light
Why do WSL2 and Windows Sandbox start in seconds and feel so light? This article explains the mechanisms, from dynamic base images and di...
The Depths of Windows Virtualization (Part 2) — Memory Even the Kernel Cannot See: How VBS, HVCI, and Credential Guard Work
On a clean install to compatible hardware, VBS is enabled by default and uses the hypervisor and SLAT to create isolation stronger than t...
The Depths of Windows Virtualization (Part 1) — Where Is Your Windows Actually Running? The Hypervisor and Partitions
When you enable Hyper-V, the host Windows itself runs on top of the hypervisor as the root partition. This article explains the foundatio...
The Win32 Thread Pool API — Concurrency Without Creating Threads, via CreateThreadpoolWork
Are you spawning CreateThread calls all over your native code? This article explains the Win32 thread pool API redesigned in Vista — the ...
Named Pipes in Practice — Windows' Standard IPC from Design to Security
A practical guide to named pipes, Windows' standard inter-process communication. This article organises, from primary sources, the choice...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Technical Consulting & Design Review
UUID collision questions span not just the spec itself but random sources, snapshot operations, DB constraints, and idempotency, so they are worth working through as a design review or technical consultation.
Bug Investigation & Root Cause Analysis
In real duplicate-ID incidents you need to determine whether the UUID itself is at fault or the implementation and operations are, so organizing the investigation angles and designing recurrence prevention is essential.
Frequently Asked Questions
Common questions about the topic of this article.
- Do UUIDs collide?
- Used in the normal way, they are unlikely enough to collide. Under RFC 9562, UUIDv4 has 122 bits of random space, and UUIDv7 is defined on the assumption that the 74 bits outside the timestamp are used for randomness or a counter that provides uniqueness. As long as you use a proper implementation in the normal way, like Python's uuid4(), the guarantees are quite strong. That said, RFC 9562 itself states that a UUID can never absolutely guarantee true global uniqueness, and that uses where a collision has high impact should take stronger countermeasures. That is exactly why a uniqueness constraint on the database side is the last line of defense.
- Why do duplicate UUIDs happen?
- Most UUID duplicates seen in practice are not a problem with the specification itself but cases where the implementation or operations break the generation conditions the spec assumes. There are six typical patterns: hand-rolling UUIDs with a fixed seed or a weak PRNG; rewinding generator state after a fork, a VM snapshot, or a container copy; misusing UUIDv3 / v5 as if they produced a new ID every time; hand-implementing time-based UUIDs or UUIDv8 and handling clock rollback or the counter carelessly; truncating the UUID along the way and throwing its uniqueness out; and leaving the database without a uniqueness constraint so duplicates slip in silently.
- Do UUIDv3 and UUIDv5 cause duplicates?
- UUIDv3 / v5 are not collision-resistant random IDs; they are deterministic IDs that regenerate the same ID from the same name. RFC 9562 requires that UUIDs generated from the same canonical name within the same namespace be equal, so getting the same UUID from the same input is not a failure but the specified behavior. Using them for fresh ID assignment is therefore a misuse. Conversely, when the canonicalization of the name drifts, the same subject ends up with a different UUID. Writing the namespace design and the name normalization rules into the specification is what matters.
- What should I do to prevent duplicate UUIDs?
- Start by not generating UUIDs yourself: move to a standard API such as uuid4() / uuid7(), or to a widely used implementation. Fix the UUID version you use in the specification, and make sure generator state is not carried over after a fork, a worker restart, a snapshot, or a clone. Keep the full 128 bits when storing and comparing, and never treat prefix comparison or a shortened display as the real key. On top of that, if duplicates truly cannot be tolerated, put a UNIQUE / PRIMARY KEY constraint in the database and make sure duplicate key errors are not swallowed, so you can trace which generator or node produced them.