Do UUIDs Collide? Implementation and Operational Patterns That Invite Duplicates

· Updated: · · 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

How to read a UUID duplicate failureA 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.The UUID specification itselfGuarantees are strong (122 bits of randomness and so on)Implementation and operationsBreak the generation conditions the spec assumesMost 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.

Where uniqueness gets shaved awayA 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.Uniqueness expected from a UUIDShaved away in generation (hand-rolled, weak randomness)Shaved away in operations (snapshot, fork)Shaved away in storage (truncation, no constraint)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.

Implementation patterns that cause UUID collisionsDiagram 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 countermeasuresrecommended fornot recommended fornot recommended fornot recommended formay causemay causemay causemay causemay causemay causemitigatesmitigatespreventsusesincompatible withincompatible withusesrequiresverified byverified byverified byverified byverified bymay causeverified byUUIDRFC 9562 (UUID Standard)CSPRNGUUIDv4 (Random-Based)Hand-Rolled UUID from Weak PRNGNew Record ID AssignmentName-Based UUID (v3/v5)UUIDv8 (Experimental/Vendor-Specific)UUID CollisionGenerator StateCounter RolloverClock RollbackUUID TruncationDatabase Unique ConstraintReseeding After forkUUIDv7 (Time-Ordered)MonotonicityNamespace UUIDCanonicalizationUUIDv1/v6 (Time-Based)

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
Properties differ by versionA 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.UUID versionv4: 122 bits of randomnessv7: time + randomness or counterv3 / v5: same input, same value (name-based)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.
UUID duplicate is foundWhere did the same value really come fromWeak generatorState was rewoundMisuse of name-based UUIDTruncated at storage timeNo uniqueness constraint on the DB sideImplementation mistake

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.

NoYesTruncatedKeptHand-rolledStandard APICarried overNo problemA duplicate key error or duplicate data is detected1. Is there a UNIQUE / PRIMARY KEY in the DB (Chapter 8)Duplicates slipped in silently with no constraint.Add the constraint first and close the entry point2. Are the full 128 bits kept in storage and comparison (Chapter 7)Suspect prefix comparison, squashing into 64 bits,or a tail cut off by an undersized column3. Is generation done through a standard API (Chapters 3 and 5)Suspect hand-rolling with a weak PRNG, ormisuse of a name-based UUID for ID assignment4. Is generator state carried over after fork / snapshot / clone (Chapter 4)The generator state has been rewoundWhat remains is a custom time-based implementationor 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.

  1. random.Random is 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.
  2. 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.
Two problems with a hand-rolled pseudo UUIDA 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.Hand-rolled with a general-purpose PRNG and a fixed seedThe same seed reproduces the same sequenceThe version / variant bits are never setThe same value appears in another process or nodeOnly 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.

Practical conclusions about generationA 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.Do not hand-roll UUIDsDo not fiddle with random seeds by handUse the standard library or a widely used implementationUsing 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

How duplicated or rewound state produces duplicatesA 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.Snapshot restore, container copy, worker forkGenerator state is carried over unchangedRandom and counter state rewindsThe generation sequence repeats and duplicates appearFix: 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.

How to read name-based UUIDs correctlyA 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.Same namespace + same canonical nameAlways the same UUID (a MUST in the spec)Used for fresh ID assignment, duplicates are per specA 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
What drifting canonicalization causesA 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.Keep one normalization function and always use itA trailing slash and the like driftNormalization of the nameDoes it driftThe same subject always gets the same IDThe same subject gets a different UUIDFix 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.

Dangerous assumptions in UUIDv1 / v6A 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.Assume a MAC address is uniqueDangerous designReplicate a node ID baked into an imageReset the clock sequence to a fixed valueIn 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.

What is risky to leave unhandled in a hand-rolled UUIDv7A 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.High-volume issuance with no counter designA hand-rolled v7 prone to duplicatesClock rollback left unhandledThe same counter initialized independentlyKnowingly 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.

What determines the number of values a counter can guaranteeA 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.Initialize to a random value on each tickAvailable values are 4096 minus the initial valueReserving the high bits guarantees a minimum count1. Decide the range allowed for initialization2. 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.

What choosing UUIDv8 meansA 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.Build a proprietary format on UUIDv8Uniqueness is implementation-specific and the spec guarantees nothingYour design document becomes the uniqueness specificationIntroducing 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.

A change of representation versus a change that removes uniquenessA 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.It keeps themIt removes someDoes the transformation keep all 128 bitsHyphens removed, case normalized, stored as binaryFirst 8 characters, 64-bit squash, truncated tailA harmless transformationA 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.

The DB uniqueness constraint as the last line of defenseA 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.Use UUIDs as IDs unlikely to collideEven so, no absolute guarantee (stated in the RFC)Make UNIQUE / PRIMARY KEY the last line of defenseDesign 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.

  • kind is p for a primary key, u for a unique constraint, and i for a unique index created with CREATE UNIQUE INDEX that has no constraint behind it.15
  • If not a single row has prevents_dup set to true, duplicates on that column are not being prevented.
The two catalog sources behind the uniqueness checkA 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.Check uniqueness on the UUID columnpg_constraint (p and u)pg_index (unique indexes)Judge single-column uniqueness with prevents_dup

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 INCLUDE columns as key columns. In CREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid), indkey also lists uuid, which is not part of the key.15 Meanwhile indnkeyatts is the number of key columns (1 in this example), so indnkeyatts = 1 and “contains uuid” hold at the same time, and an index built on id ends up counted as “preventing duplicate uuid values.” The key is only the first indnkeyatts entries of indkey, so match against those alone (indkey is 0-based15)
  • Counting a partial index as a whole-table guarantee. CREATE UNIQUE INDEX ... ON orders(uuid) WHERE active guarantees uniqueness only among the rows that match the condition. The same UUID can coexist quite happily between two rows that are not active, or between a row in the index and a row outside it. A non-null indpred means 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 the WHERE clause in definition and treat it as a conditional line of defense)
Two traps that produce false positives in a uniqueness auditA 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.Inspect by naming the UUID columnPick up both constraints and indexesLook at prevents_dupPer-table counting misreportsFalse 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.

The order to suspect things in when you find a duplicateA 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.A duplicate is found1. Suspect the generator2. Suspect state management3. Suspect the storage format4. Suspect the constraint designThe 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.

12. References

  1. IETF RFC 9562, Section 5.4 UUID Version 4. On the 122-bit random space of UUIDv4.  2

  2. IETF RFC 9562, Section 5.7 UUID Version 7. On the design of UUIDv7’s timestamp, random bits, and counter.  2 3 4

  3. IETF RFC 9562, Section 5.8 UUID Version 8. On UUIDv8 uniqueness being implementation-specific and not to be assumed.  2 3

  4. Python 3.14 documentation, uuid module. On uuid4()’s cryptographically-secure generation, uuid5()’s deterministic behavior, and the properties of uuid7() / uuid8() 2 3

  5. IETF RFC 9562, Universally Unique IDentifiers (UUIDs). The baseline document for the UUID format, each version, and best practices overall.  2

  6. PostgreSQL documentation, Constraints. On guaranteeing uniqueness via UNIQUE constraints and PRIMARY KEY.  2

  7. 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

  8. IETF RFC 9562, Section 6.9 Unguessability. On CSPRNG use and re-seeding after fork.  2 3

  9. IETF RFC 9562, Section 6.3 UUID Generator States. On handling stable storage and generator state.  2 3

  10. IETF RFC 9562, Section 5.5 UUID Version 5. On the specification of name-based UUIDs built from a namespace plus a canonical name. 

  11. IETF RFC 9562, Section 5.6 UUID Version 6. On UUIDv6’s node / clock sequence / DB locality. 

  12. IETF RFC 9562, Section 6.4 Distributed UUID Generation. On node collision resistance in distributed environments. 

  13. IETF RFC 9562, Section 6.2 Monotonicity and Counters. On cautions around clock rollback, counter rollover, and batch generation.  2 3 4

  14. IETF RFC 9562, Sections 6.7 and 6.8. On the thinking behind collision resistance and global uniqueness. 

  15. PostgreSQL documentation, pg_constraint. On contype being p for a primary key and u for a unique constraint, conrelid pointing at the table the constraint applies to, and conindid pointing at the index that backs the constraint. A unique index with no constraint behind it appears only on the pg_index side (indrelid is the target table, indisunique says whether it is unique).  2 3 4

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog