¿Los UUID no colisionan? - Patrones de implementación y operación incorrectos que provocan duplicados

· Actualizado el: · · UUID, Identificadores, Sistemas distribuidos, Diseño de datos, Implementación

Tenía UUID como clave primaria y, un día, aparece un duplicate key. En ese momento, es muy probable que surja la idea de que «al final, los UUID sí chocan».

Sin embargo, la mayoría de las duplicaciones de UUID que se producen en la práctica no son tanto un problema del propio estándar UUID como casos en los que la implementación o la operación rompen las condiciones de generación que el estándar da por sentadas. Según RFC 9562, UUIDv4 tiene un área aleatoria de 122 bits, y UUIDv7 también se define suponiendo que, aparte de la marca de tiempo, los 74 bits restantes se usan como aleatoriedad o contador para la unicidad. Por otro lado, se establece explícitamente que UUIDv8 «depende de la implementación y no debe asumirse que garantiza unicidad».123 Además, la propia biblioteca estándar de Python explica que uuid4() se genera mediante un método criptográficamente seguro, así que, al menos mientras se «use con normalidad una implementación seria», la premisa del lado del UUID es bastante sólida.4

En este artículo se organizan los patrones típicos en los que una operación o implementación incorrecta hace que los UUID colisionen, junto con las medidas para evitar que vuelva a ocurrir. El contenido se basa en RFC 9562, la documentación oficial de Python y la documentación oficial de PostgreSQL, tal como podían consultarse en marzo de 2026.546

Terminología usada en este artículo

A continuación se traducen, en una línea cada uno, los términos que aparecerán en inglés en los capítulos siguientes.

Término Significado en una línea
PRNG pseudo-random number generator, generador de números pseudoaleatorios. A partir de un seed genera una secuencia determinada, de modo que el mismo seed reproduce la misma secuencia.
CSPRNG cryptographically secure PRNG, generador de números pseudoaleatorios criptográficamente seguro. Su objetivo de diseño incluye que el siguiente valor sea difícil de predecir.
generator state Estado del generador. Es el material que determina el siguiente UUID: el estado interno del generador aleatorio, el clock sequence, el contador, etc.
carefully seeded counter Contador inicializado con cuidado. En UUIDv7 designa el campo que se usa para generar la secuencia dentro del mismo milisegundo.
clock rollback Retroceso del reloj. Ocurre cuando la hora del sistema vuelve a un valor anterior, por una corrección NTP o un cambio manual.
counter rollover Desbordamiento del contador. Ocurre cuando el contador supera su valor máximo y vuelve a 0.
monotonicity Monotonía. Propiedad por la que un UUID generado más tarde siempre tiene un valor mayor.
namespace Espacio de nombres. En UUIDv3 / v5 es el UUID que fija el contexto en el que se interpreta el name.
canonicalization Normalización. Proceso que unifica en una sola forma las cadenas que designan el mismo objeto, incluyendo mayúsculas/minúsculas y la barra final.

1. La conclusión primero

Para resumir brevemente por adelantado, los patrones peligrosos son estos.

Patrón Qué ocurre Primera medida a tomar
Fabricar a mano un valor con aspecto de UUIDv4 usando un seed fijo o un PRNG débil Otro proceso u otro nodo reproduce la misma secuencia Usar la API de UUID estándar del sistema operativo o del runtime
Heredar tal cual el estado de generación tras un fork, un snapshot de VM o una réplica de contenedor El estado del aleatorio o del contador retrocede y aparecen duplicados Volver a hacer seed tras el fork, reinicializar tras el clone y revisar el tratamiento del estado persistente
Usar UUIDv3 / v5 creyendo que dan «un ID nuevo cada vez» El mismo namespace y el mismo name regeneran el mismo UUID Entender que es un ID determinista y limitar su uso
Implementar UUIDv1 / v6 / v7 / v8 por cuenta propia y tratar con descuido el clock rollback o el node/counter Aumenta el riesgo de duplicados con generación de alta frecuencia o varios nodos Usar bibliotecas existentes y reducir los generadores propios
Truncar el UUID a mitad de camino o reducirlo a otro formato Se descarta por cuenta propia la unicidad original de 128 bits Guardar y comparar siempre con la longitud completa
No poner UNIQUE / PRIMARY KEY en la base de datos Los duplicados entran en silencio y se retrasa la investigación de la causa Tener una restricción de unicidad en la capa de almacenamiento

En resumen, más que decir que el UUID colisionó, en muchos casos el propio diseño va recortando por el camino la unicidad que se esperaba del UUID.

2. Lo primero que hay que sospechar no son «las matemáticas del UUID», sino «la generación y la operación»

El tema de los UUID se complica porque las propiedades cambian según la versión.

  • UUIDv4 está basado en aleatoriedad. Según RFC 9562, los 122 bits que quedan fuera de version/variant se rellenan con valores aleatorios(Section 5.4).1
  • UUIDv7 tiene una estructura fácil de ordenar cronológicamente: además de la marca de tiempo Unix en milisegundos, el resto se compone de aleatoriedad o de un carefully seeded counter(Section 5.7).2
  • UUIDv3 / v5 son name-based. Con el mismo namespace y el mismo canonical name, que salga el mismo UUID es el comportamiento correcto(Section 6.5).7
  • UUIDv8 es de uso experimental o específico del proveedor, y su unicidad depende de la implementación. El RFC indica que «no debe asumirse que garantiza unicidad»(Section 5.8).3

Es decir, aunque se diga que «se usa UUID», lo que hay por dentro puede ser

  • ¿el uuid4() de la biblioteca estándar?
  • ¿un timestamp + random propio?
  • ¿un uuid5(namespace, name)?
  • ¿un formato propio que solo se parece a UUIDv8?

y según cuál sea, la conversación cambia por completo.

Diagrama de causas de una duplicación de UUIDDiagrama que muestra que una duplicación de UUID detectada puede deberse a un generador débil, un estado que retrocedió, un uso indebido de UUID name-based, un truncamiento al guardar o la falta de una restricción de unicidad en la base de datos, y que todas estas causas terminan siendo un error de implementaciónSe detecta una duplicación de UUID¿Dónde coincidió realmente el valor?Generador débilEl estado retrocedióUso indebido de UUID name-basedTruncamiento al guardarSin restricción de unicidad en la BDError de implementación

Figura 1: Diagrama de causas de una duplicación de UUID.

En la práctica, es más rápido revisar este diagrama empezando por la derecha.

Si se escribe un poco más como procedimiento, lo práctico es ir descartando causas empezando por las que cuestan menos de comprobar. Con el siguiente orden, las herramientas necesarias van aumentando por etapas —«ejecutar una consulta SQL», «hacer grep en el código», «revisar el procedimiento operativo»—, así que en cuanto se encuentra la causa se puede parar ahí.

Flujo de diagnóstico de una duplicación de UUID por coste de verificaciónDiagrama que ordena la comprobación de una restricción de unicidad en la base de datos, el mantenimiento de los 128 bits completos, el uso de la API estándar de generación y la herencia del estado del generador tras fork, snapshot o clone, de menor a mayor coste de verificaciónNoTruncaMantieneImplementación propiaAPI estándarLa heredaSin problemasSe detecta un duplicate key o datos duplicados1. ¿Hay UNIQUE / PRIMARY KEY en la base de datos(capítulo 8)?Los duplicados entraban en silencio sin restricción.Primero, cree la restricción para cerrar la puerta de entrada2. ¿Se mantienen los 128 bits completos al guardar y comparar(capítulo 7)?Sospeche de la comparación por prefijo, la reducción a 64 bitso el corte final por longitud de columna insuficiente3. ¿La generación usa la API estándar(capítulos 3 y 5)?Sospeche de una implementación propia con PRNG débil, ode un uso indebido de UUID name-based para emitir nuevos IDs4. ¿Se hereda el estado del generador tras un fork / snapshot / clone(capítulo 4)?El estado del generador ha retrocedidoLo que queda es una implementación propia basada en tiempo,o el propio diseño de UUIDv8(capítulo 6)

Figura 2: Flujo de diagnóstico de una duplicación de UUID por coste de verificación.

3. Patrón 1: decir que es UUIDv4, pero usar en realidad un PRNG débil

Esto es lo más habitual.

  • Generar 128 bits con un PRNG de propósito general equivalente a Math.random()
  • Sembrar con time() o el PID en el arranque
  • Ensamblar a mano «32 dígitos hexadecimales con forma de UUID»

Aunque parezca un UUID, si la fuente de aleatoriedad es débil, la misma secuencia se reproduce en otro proceso o en otro nodo.

Se entiende mejor en código. A continuación se muestra, usando solo la biblioteca estándar de Python 3, el ejemplo de lo que no se debe hacer.

# Mal ejemplo: usar un PRNG de propósito general con un seed fijo y ensamblar a mano una cadena con forma de UUID
import random


def make_pseudo_uuid(seed: int) -> str:
    rng = random.Random(seed)          # con el mismo seed, la secuencia es exactamente igual cada vez
    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: en otro proceso o en otro nodo, el mismo seed produce el mismo valor

En realidad, este ejemplo tiene dos problemas.

  1. random.Random es un PRNG de propósito general, así que con el mismo seed la secuencia se reproduce tal cual. Aunque se use como seed la hora de arranque o el PID, puede haber colisión con arranques simultáneos o clones.
  2. Como no se fija en absoluto ningún bit de version ni de variant, esto ni siquiera es un UUIDv4 según RFC 9562. Lo exacto sería decir que es solo un número de 128 bits con forma de UUID.

En cambio, el buen ejemplo resulta sorprendentemente corto.

# Buen ejemplo: use tal cual la API estándar. No toque ni el seed ni la versión
import uuid

new_id = uuid.uuid4()
print(new_id.version)    # 4: los bits de version los rellena correctamente la propia API
print(uuid.uuid4() != uuid.uuid4())   # True: cada llamada produce un valor distinto

RFC 9562 indica que, tanto para la unicidad como para la imprevisibilidad del UUID, se debería usar un CSPRNG(Section 6.9 Unguessability). Es un requisito recomendado (SHOULD), y según el uso puede haber diseños de excepción, pero si se fabrica un UUID a mano con un PRNG de propósito general, conviene poder explicar el motivo. Además, escribe que al cambiar de estado, como en un process fork, se debería volver a hacer seed correctamente del estado del CSPRNG.8 La documentación de uuid.uuid4() de Python también explica que genera un UUID aleatorio mediante un método criptográficamente seguro.4

La conclusión práctica aquí es simple.

  • No fabricar UUID a mano
  • No manipular a mano el seed aleatorio
  • Usar tal cual la biblioteca estándar o una implementación ampliamente usada

Seguir manteniendo un generador propio porque «es liviano» o porque «se usa desde hace tiempo» es lo que termina saliendo más caro.

4. Patrón 2: hacer retroceder el estado del generador con fork, snapshot o clone

El segundo patrón peligroso es una operación en la que el estado del generador se duplica o retrocede.

RFC 9562 recomienda explícitamente volver a hacer seed tras un fork(Section 6.9), y explica que una implementación sin stable storage aumenta la frecuencia de generación de clock sequence, counter y random data, lo que eleva la probabilidad de duplicados(Section 6.3 UUID Generator States).89

De aquí se desprende de forma natural un razonamiento práctico.

  • Restaurar varias veces la misma imagen tras tomar un snapshot de VM
  • Que el generador propio arranque desde el mismo estado inicial al iniciar la imagen del contenedor
  • Compartir el estado del PRNG o del contador tras un fork de worker

En estas operaciones, la secuencia de generación de UUID puede reproducirse sin querer. El RFC no dice literalmente que «el snapshot sea peligroso», pero esta es una advertencia bastante práctica que se puede derivar de las notas sobre el re-seed tras el fork y sobre el tratamiento del generator state.89

Las medidas van por aquí.

  • No mantener durante mucho tiempo un estado de generación de UUID propio
  • Reinicializar inmediatamente después de un fork / clone / restore
  • Cuando sea posible, orientarse hacia una implementación que use en cada ocasión aleatoriedad proveniente del sistema operativo
  • Si es un generador de alta frecuencia, documentar por escrito la gestión del estado y las reglas de re-seed

5. Patrón 3: confundir UUIDv3 / v5 con «un ID nuevo cada vez»

UUIDv3 / v5 no son IDs aleatorios resistentes a colisiones. Son IDs deterministas que regeneran el mismo ID a partir del mismo nombre.

RFC 9562 establece que el UUID generado a partir del mismo same name, con el mismo canonical format, en el mismo same namespace, debe ser igual(Section 6.5 Name-Based UUID Generation).7 Por eso, si se usa de esta manera, la duplicación no es un accidente: es el comportamiento previsto por la especificación.

  • Usar uuid5(NAMESPACE_URL, "https://example.com/users/42") cada vez como «numeración nueva»
  • Emitir con un namespace común a todos los clientes + email, sin poner el tenant en el namespace
  • Suponer que, al reemitir el mismo nombre lógico en cada retry, saldrá un ID distinto

Esto también se puede comprobar con un código breve. Funciona solo con la biblioteca estándar de Python 3.

# Ejemplo de mal uso: usar uuid5 pensando que es «numeración nueva»
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: da igual cuántas veces se llame, siempre es igual. Es el comportamiento previsto por la especificación

# Además, si la normalización del name varía, ahora ocurre lo contrario: sale un ID distinto
with_slash = uuid.uuid5(uuid.NAMESPACE_URL, url + "/")
print(first == with_slash)   # False: una sola barra final produce un UUID distinto

# Si lo que se quiere es numeración nueva, la versión que corresponde es otra
print(uuid.uuid4() != uuid.uuid4())   # True

Si se usa uuid5, lo seguro es decidir de antemano cómo normalizar el name antes de pasarlo y concentrar esa función de normalización en un solo lugar.

# Buen ejemplo: extraiga la canonicalization a una función y páselo siempre por ella antes de llamar a uuid5
import uuid

# Separe el namespace por tenant. Fije este valor como parte de la especificación y no lo cambie después
TENANT_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "tenant-a.example.com")


def canonical_user_url(user_id: int) -> str:
    # Decida una única forma, incluyendo el esquema, el host y si hay o no barra final
    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: para el mismo objeto, siempre el mismo ID

Por el contrario, si la canonicalization del name varía, se obtiene un UUID distinto para el mismo objeto. El propio RFC insiste repetidamente en el tratamiento de la canonical representation(Section 5.5, Section 6.5).710

Lo importante en esta línea es:

  • UUIDv3 / v5 no son «numeración sin duplicados», sino «la misma entrada produce el mismo ID»
  • No dejar ambiguo el diseño del namespace
  • Especificar formalmente la canonicalization del name

estos tres puntos.

6. Patrón 4: implementar por cuenta propia UUID basados en tiempo o UUIDv8

UUIDv1 / v6 / v7 / v8 son peligrosos si solo se imita su apariencia.

6.1 Tratar el node o el clock sequence con descuido en UUIDv1 / v6

Según RFC 9562, UUIDv6 es un reordenamiento de UUIDv1 para mejorar la DB locality, y maneja el clock sequence y el node(Section 5.6). Además, hay varias advertencias sobre la node collision resistance en entornos distribuidos(Section 6.4)y sobre la conservación del state(Section 6.3).11912

Es más, el RFC llega a decir que con la aparición de las máquinas virtuales y los contenedores, la unicidad de la MAC address ya no está garantizada.5

Por eso,

  • Dar por sentado que, por ser una dirección MAC, es única
  • Duplicar el node ID grabándolo en la imagen
  • Volver el clock sequence a un valor fijo en cada reinicio

son diseños peligrosos.

6.2 Implementar UUIDv7 por cuenta propia y dejar sin resolver el counter rollover o el clock rollback

UUIDv7 es bastante práctico, pero el RFC describe con detalle la monotonicity y el counter handling en la generación de alta frecuencia(Section 6.2 Monotonicity and Counters). También se indica explícitamente que no se debe devolver un duplicado a sabiendas (knowingly) por un clock rollback o un counter rollover.213

Es decir, que

  • Emitir grandes volúmenes dentro del mismo milisegundo sin un diseño de counter
  • Seguir generando sin hacer nada cuando el reloj retrocede
  • Que varios procesos inicialicen por separado el mismo internal counter

son implementaciones peligrosas.

¿A partir de qué frecuencia de emisión hay que preocuparse?

Para juzgar si «esto se aplica a mi sistema», lo más rápido es mirar la asignación de bits.

El UUIDv7 de RFC 9562 tiene una estructura en la que, tras los 48 bits de marca de tiempo en milisegundos, sigue rand_a(12 bits), y tras el variant sigue rand_b(62 bits).2 Y en Section 6.2 se presentan, como formas de mantener la monotonía, el método de convertir los 12 bits de rand_a en un contador dedicado(Method 1), el método de usar el lado de rand_b como «un contador inicializado aleatoriamente»(Method 2)y el método de sustituir hasta los 12 bits de rand_a por una precisión temporal más fina que el milisegundo(Method 3).13

A partir de aquí, la referencia se puede leer así.

Aquí, no lea sin más que «12 bits = se pueden usar hasta 4096 unidades».RFC 9562 exige inicializar con un valor aleatorio en cada tick para que el contador sea difícil de predecir.13 Si el valor inicial es 4000, en ese tick solo quedan disponibles 96 unidades. La cantidad que se puede usar dentro de 1 tick es «4096 menos el valor inicial», no 4096.

Como medida frente a esto, el RFC menciona el método de dejar en 0 los bits altos del contador e inicializar solo el lado bajo.13 Por ejemplo, si se fija en 0 el bit más alto y solo se hace aleatorio los 11 bits bajos, el valor inicial es como máximo 2047, así que en cualquier tick quedan garantizadas al menos 2048 unidades. La cantidad que se puede garantizar la determina el margen de inicialización, no el ancho del contador.

Con esto en cuenta, la referencia queda así.

Emisiones por generador y por milisegundo Cómo pensarlo
De unas pocas a unas decenas Con una inicialización que reserve los bits altos, no se agota dentro de 1 tick
Unos cientos Según cómo se inicialice, aquí puede darse una vuelta completa.Es el nivel en el que conviene fijar el límite superior del valor inicial y documentar como especificación el comportamiento en el rollover (avanzar la marca de tiempo o esperar)
1000 o más Aunque se reserven los bits altos, el margen se vuelve escaso. Dé por supuesto el Method 2, que también usa el lado de rand_b, o un diseño que avance la marca de tiempo

No se puede decir que «como no se emiten millones por segundo, el rollover no importa».El límite lo determina el diseño de la inicialización, así que incluso con una frecuencia de emisión media puede darse una vuelta completa si el valor inicial se toma de todo el rango de 12 bits. Si se despliega sin decidir qué hacer cuando ocurre el rollover, ahí aparecerán los duplicados.Diseñe primero decidiendo el margen de inicialización y después el comportamiento en el rollover, en ese orden.

Aun así, hay dos puntos en los que no conviene quedarse tranquilo.

  • El clock rollback ocurre independientemente de la frecuencia de emisión. Es normal que la hora retroceda por una corrección NTP, por la reanudación de una VM suspendida o por un cambio manual. Incluso un sistema que solo genera 10 unidades por segundo necesita tomar medidas.
  • Como es «por generador», hay que multiplicar por el número de procesos. Si 100 procesos tienen cada uno su propio contador de forma independiente y, además, todos empiezan desde el mismo valor inicial, las secuencias se solapan aunque el número de emisiones por proceso sea bajo.

6.3 Usar UUIDv8 con la ligereza de «un nuevo estándar de UUID»

UUIDv8 parece útil a simple vista, pero RFC 9562 es bastante claro: establece que la unicidad de UUIDv8 depende de la implementación y no debe darse por sentada(Section 5.8).3

Por eso,

  • incrustar un timestamp
  • incrustar un shard id
  • incrustar algún significado de negocio
  • rellenar el resto con aleatoriedad más o menos al azar

un «UUID propio de la empresa» de este tipo hace que el propio documento de diseño sea la especificación de unicidad del UUID. Es demasiado peligroso introducirlo sin revisión.

7. Patrón 5: acortar el UUID en algún punto del camino

Aunque la generación sea correcta, puede romperse en la etapa de almacenamiento o comparación.

Estos son ejemplos típicos.

  • Usar solo los primeros 8 caracteres como sustituto de clave foránea
  • Reducir un UUID de 128 bits a un entero de 64 bits
  • Que la longitud de la columna de texto no alcance y se corte el final
  • Tratar como clave única, tal cual, la representación abreviada usada en logs o en pantalla

Lo importante aquí es que cambiar la representación no es malo en sí mismo.

  • Quitar los guiones
  • Unificar mayúsculas y minúsculas
  • Guardarlo como 16 bytes binarios

Conversiones como estas, que no pierden ninguno de los 128 bits, no son ningún problema. Lo peligroso son las conversiones que recortan el propio material de la unicidad.

En particular, es fácil que se convierta en un accidente el diseño en el que se crea aparte un «ID abreviado fácil de leer para las personas» y, sin darse cuenta, termina teniendo prioridad sobre el UUID original.

8. Patrón 6: no tener una restricción de unicidad en la base de datos

Y esto es especialmente importante.

Aunque el UUID sea suficientemente resistente a colisiones, si de verdad no se pueden tolerar duplicados, también se debería tener una restricción de unicidad en el destino de almacenamiento.

La documentación oficial de PostgreSQL explica que un unique constraint garantiza que el valor de una columna o de un conjunto de columnas sea único en toda la tabla, y que la primary key se convierte en un identificador de fila unique y not null.6

RFC 9562 también establece que, si bien el UUID puede ofrecer una unicidad suficiente en la práctica, no puede garantizar de forma absoluta una global uniqueness verdadera. Y añade que, en usos donde el collision impact es alto, se deberían tomar medidas más fuertes(Section 6.7 Collision Resistance, Section 6.8 Global and Local Uniqueness).14

En la práctica, esta combinación es la base.

  • Usar el UUID como un ID resistente a colisiones
  • Tener en la base de datos una última línea de defensa con UNIQUE / PRIMARY KEY
  • Diseñar el retry, la idempotency y el incident logging para el caso de duplicado

Usar UUID y no poner una restricción de unicidad no son la misma cosa.

9. Lista de verificación para uso práctico

Por último, se resume en una forma fácil de usar tal cual para la implantación o la auditoría. Mientras que la tabla de conclusiones del capítulo 1 era un listado de «qué es peligroso», esta es un listado de cómo comprobar el propio sistema.

# Qué comprobar Cómo comprobarlo Criterio de aprobación
1 Si se generan UUID por cuenta propia Hacer grep en todo el repositorio de rastros de ensamblado manual como getrandbits, Math.random, new Random(, %032x La generación de UUID pasa únicamente por APIs estándar como uuid4() / uuid7()
2 Si la version del UUID está fijada como especificación Contar la version a partir de los datos ya guardados. En PostgreSQL, substring(id::text from 15 for 1) es el dígito de la version La version permitida está documentada y los datos reales coinciden con ella
3 Si se ha inventariado el tratamiento del seed y del generator state Leer si en los scripts de arranque, el Dockerfile o los procedimientos de snapshot / clone hay una descripción de «reinicialización». Hacer grep de los puntos donde se hace fork de un worker Está documentado explícitamente el procedimiento para recrear el generador justo después de un fork, un reinicio de worker, un snapshot o un clone
4 Si se mantiene la longitud completa al guardar Comprobar la definición de la columna. Además, hacer grep en el código de truncamientos como [:8], substring(, Left(, ToString("N").Substring Tanto el almacenamiento como la comparación se mantienen en 128 bits. La representación abreviada se limita solo a la visualización
5 ¿Hay UNIQUE / PRIMARY KEY en la base de datos? Con el SQL de abajo, indicando el nombre de la columna que contiene el UUID, enumerar tanto las restricciones como los índices únicos (si se cuenta por tabla, se termina contando también la primary key del id secuencial) Para esa columna hay al menos una fila con single_col en true
6 Si se puede observar un duplicado Hacer grep de los puntos donde se suprime una excepción equivalente a duplicate key. Comprobar en la práctica si el log muestra el generator / node / deployment Al producirse un duplicado se registra la excepción y se puede rastrear dónde ocurrió

Para la comprobación del punto 5, en PostgreSQL basta con la siguiente consulta.

-- Enumera si la columna uuid de public.orders tiene una restricción de unicidad.
-- Sustituya el nombre de tabla y de columna por los del objetivo
WITH target AS (
    SELECT attrelid, attnum
    FROM   pg_attribute
    WHERE  attrelid = 'public.orders'::regclass
      AND  attname  = 'uuid'                    -- ← columna a investigar
      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, -- si esa columna por sí sola evita duplicados
       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)                -- solo las que incluyen esa columna en la clave
UNION ALL
SELECT i.relname                     AS name,
       'i'                           AS kind,      -- i = índice único sin restricción asociada
       -- Un índice parcial (indpred no NULL) solo garantiza unicidad entre las filas que cumplen la condición
       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 (                                 -- Las columnas INCLUDE no son clave, así que no se cuentan.
         SELECT 1                               -- mira solo los primeros indnkeyatts elementos de indkey
         FROM   generate_series(0, x.indnkeyatts - 1) AS k(i)  -- indkey empieza en 0
         WHERE  x.indkey[k.i] = t.attnum)
  AND  NOT EXISTS (                             -- los índices ligados a una restricción ya se listaron arriba
         SELECT 1 FROM pg_constraint AS c2 WHERE c2.conindid = x.indexrelid);

La lectura se hace en dos pasos.

  • kind vale p para la primary key, u para el unique constraint y i para un índice único sin restricción asociada creado con CREATE UNIQUE INDEX.15
  • Si no hay ninguna fila con prevents_dup en true, el duplicado de esa columna no está evitado.

No juzgue por «si la tabla tiene una restricción de unicidad».Es bastante habitual el diseño en el que la primary key está solo en el id secuencial y la columna UUID queda sin restricción. Si se cuenta por tabla, ese estado se reporta erróneamente como «hay última línea de defensa». Por la misma razón, aunque exista una clave compuesta que incluya el UUID, el duplicado del UUID por sí solo pasa igualmente, así que hay que fijarse en prevents_dup.

Otra cosa: no mire solo pg_constraint.También es habitual el diseño en el que la unicidad se introduce con CREATE UNIQUE INDEX, y eso solo aparece en pg_index. Si se cuentan solo las restricciones y se reporta que «no hay última línea de defensa», se pasa por alto un índice que ya existe y se avanza hacia un cambio de esquema innecesario. La consulta de arriba recoge ambas formas (indnkeyatts está disponible desde PostgreSQL 11).

Si el criterio del lado del índice se escribe simplemente como «si esa columna está en indkey», produce dos tipos de falsos positivos. Ambos se inclinan hacia reportar erróneamente que «está evitado», así que se termina dando el visto bueno tal cual.

  • Contar una columna INCLUDE como clave.En el indkey de CREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid) también aparece uuid, que no es clave.15 Por otro lado, indnkeyatts es el número de columnas clave (1 en este ejemplo), así que se cumplen a la vez indnkeyatts = 1 y que incluye uuid, con lo que el índice creado sobre id termina apareciendo como si «evitara el duplicado de uuid».La clave son solo los primeros indnkeyatts elementos de indkey, así que la comprobación se limita a ellos (los índices de indkey empiezan en 015)
  • Contar un índice parcial como garantía global.CREATE UNIQUE INDEX ... ON orders(uuid) WHERE active solo garantiza la unicidad entre las filas que cumplen la condición. El mismo UUID puede coexistir con normalidad entre filas que no son active, o entre una fila que está en el índice y otra que no lo está. Si indpred no es NULL, se trata de un índice parcial,15 así que no se cuenta como prevención de duplicados a nivel global (el índice en sí queda en el listado, así que se puede tratar como una defensa condicional revisando la cláusula WHERE de definition)

10. Resumen

Los incidentes de colisión de UUID suelen empezar no porque el UUID sea débil, sino porque la implementación o la operación rompe las premisas del UUID.

  • Fabricarlo a mano con aleatoriedad débil
  • Hacer retroceder el estado tras un fork o un snapshot
  • Usar un UUID name-based para numeración nueva
  • Implementar v7 o v8 por cuenta propia sin cuidado
  • Acortarlo a mitad de camino y descartar la unicidad
  • Quitar la restricción de unicidad del lado de la base de datos

Si se hace algo de esto, es casi lo mismo que ir a crear uno mismo una situación propensa a colisiones.

Si se encuentra un duplicado, lo primero que hay que sospechar no son las matemáticas del UUID, sino el generador, la gestión del estado, el formato de almacenamiento y el diseño de restricciones. Revisando en ese orden, casi siempre se puede acotar bastante la causa.

11. Artículos relacionados

12. Referencias

  1. IETF RFC 9562, Section 5.4 UUID Version 4. Sobre el área aleatoria de 122 bits de UUIDv4.  2

  2. IETF RFC 9562, Section 5.7 UUID Version 7. Sobre el criterio de diseño del timestamp, los random bits y el counter de UUIDv7.  2 3 4

  3. IETF RFC 9562, Section 5.8 UUID Version 8. Sobre que la unicidad de UUIDv8 depende de la implementación y no debe darse por sentada.  2 3

  4. Documentación de Python 3.14, módulo uuid. Sobre la cryptographically-secure generation de uuid4(), el deterministic behavior de uuid5() y las propiedades de uuid7() / uuid8() 2 3

  5. IETF RFC 9562, Universally Unique IDentifiers (UUIDs). Documento de referencia general sobre el formato del UUID, cada version y las best practices.  2

  6. Documentación de PostgreSQL, Constraints. Sobre cómo el UNIQUE constraint y la PRIMARY KEY garantizan la unicidad.  2

  7. IETF RFC 9562, Section 6.5 Name-Based UUID Generation. Sobre que el same namespace y el same name producen el mismo UUID, y sobre la importancia de la canonicalization.  2 3

  8. IETF RFC 9562, Section 6.9 Unguessability. Sobre el uso del CSPRNG y el re-seed tras un fork.  2 3

  9. IETF RFC 9562, Section 6.3 UUID Generator States. Sobre el tratamiento del stable storage y del generator state.  2 3

  10. IETF RFC 9562, Section 5.5 UUID Version 5. Sobre la especificación del UUID name-based basado en el namespace y el canonical name. 

  11. IETF RFC 9562, Section 5.6 UUID Version 6. Sobre el node, el clock sequence y la DB locality de UUIDv6. 

  12. IETF RFC 9562, Section 6.4 Distributed UUID Generation. Sobre la node collision resistance en entornos distribuidos. 

  13. IETF RFC 9562, Section 6.2 Monotonicity and Counters. Sobre las advertencias relativas al clock rollback, el counter rollover y la batch generation.  2 3 4

  14. IETF RFC 9562, Sections 6.7 and 6.8. Sobre el criterio de la collision resistance y la global uniqueness. 

  15. Documentación de PostgreSQL, pg_constraint. Sobre que contype vale p para primary key y u para unique constraint, que conrelid señala la tabla objeto de la restricción, y que conindid señala el índice que sostiene la restricción. Un índice único sin restricción asociada aparece únicamente en pg_index (indrelid es la tabla objeto, indisunique indica si es único).  2 3 4

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Consultoría técnica y revisión de diseño

El tema de la colisión de UUID no se limita a entender el estándar, sino que abarca también la fuente de aleatoriedad, la operación de snapshots, las restricciones de la base de datos y la idempotency, por lo que vale la pena tratarlo como una revisión de diseño o una consultoría técnica.

Investigación de fallos y causas

En un incidente real de duplicación es necesario separar si «el problema es el UUID» o si «el problema es la implementación o la operación», por lo que es importante ordenar los puntos de investigación y diseñar medidas para evitar que se repita.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Los UUID no colisionan?
En un uso normal son suficientemente resistentes a colisiones. Según RFC 9562, UUIDv4 tiene un área aleatoria de 122 bits, y UUIDv7 también se define suponiendo que, aparte de la marca de tiempo, los 74 bits restantes se usan como aleatoriedad o contador para la unicidad. Mientras se use con normalidad una implementación seria, como uuid4() de Python, la premisa es bastante sólida. Sin embargo, el propio RFC 9562 establece que el UUID no puede garantizar de forma absoluta una global uniqueness verdadera, y que en usos donde el impacto de una colisión es grande se deberían tomar medidas más fuertes. Por eso la restricción de unicidad del lado de la base de datos se convierte en la última línea de defensa.
¿Por qué se producen duplicados de UUID?
La mayoría de los duplicados de UUID que ocurren en la práctica no son un problema del propio estándar, sino casos en los que la implementación o la operación rompen las condiciones de generación que el estándar da por sentadas. Los patrones típicos son seis: fabricar el UUID a mano con un seed fijo o un PRNG débil; hacer retroceder el estado del generador tras un fork, un snapshot de VM o una réplica de contenedor; usar UUIDv3 / v5 creyendo que dan «un ID nuevo cada vez»; implementar por cuenta propia UUID basados en tiempo o UUIDv8 y tratar con descuido el clock rollback o el counter; truncar el UUID a mitad de camino y descartar la unicidad; y no poner una restricción de unicidad en la base de datos, con lo que los duplicados entran en silencio.
¿Usar UUIDv3 o UUIDv5 produce duplicados?
UUIDv3 / v5 no son IDs aleatorios resistentes a colisiones, sino IDs deterministas que regeneran el mismo ID a partir del mismo nombre. RFC 9562 establece que el UUID generado a partir del mismo namespace y el mismo canonical name debe ser igual, así que obtener el mismo UUID a partir de la misma entrada no es un accidente, sino el comportamiento previsto por la especificación. Por lo tanto, usarlo para numeración nueva es un uso indebido. A la inversa, si la canonicalization del name varía, se obtiene un UUID distinto para el mismo objeto. Es importante documentar formalmente, como especificación, el diseño del namespace y las reglas de normalización del name.
¿Qué se debe hacer para evitar duplicados de UUID?
En primer lugar, no generar el UUID por cuenta propia, sino orientarse hacia una API estándar como uuid4() / uuid7() o una implementación ampliamente usada. Fijar como especificación la version de UUID que se va a usar, y evitar que el estado del generador se herede tras un fork, un reinicio de worker, un snapshot o un clone. Mantener la longitud completa de 128 bits al guardar y comparar, y no usar la comparación por prefijo ni la representación abreviada como clave real. Además, si de verdad no se pueden tolerar duplicados, poner una restricción UNIQUE / PRIMARY KEY en la base de datos, sin suprimir el duplicate key, de modo que se pueda rastrear en qué generator o node se produjo.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog