La decisión UUID vs ULID importa más de lo que crees
Elegir entre UUID vs ULID (o Snowflake, o nanoid, o auto-increment) parece una decisión trivial hasta que tienes 500 millones de filas y tu base de datos se arrastra. El formato del ID afecta la fragmentación de índices, el orden de clasificación, el tamaño de almacenamiento, la probabilidad de colisión, y si puedes extraer un timestamp del ID sin consultar la base de datos. He visto equipos migrar formatos de ID a escala — es doloroso y caro. Elige bien desde el principio.
La versión corta: UUIDv4 es aleatorio y tiene soporte universal pero fragmenta los índices B-tree. ULID tiene prefijo de timestamp y se ordena cronológicamente pero no es un estándar oficial. Los Snowflake IDs son enteros de 64 bits con timestamps embebidos pero requieren un servicio de coordinación. Auto-increment es simple pero filtra información y no funciona en sistemas distribuidos. Cada uno tiene su punto ideal.
Esta guía los compara en las dimensiones que realmente importan en producción: rendimiento de base de datos, capacidad de ordenamiento, resistencia a colisiones, filtración de información y soporte del ecosistema. Te daré un árbol de decisión al final, pero primero necesitas entender por qué existen estos tradeoffs.
UUIDv4: la opción por defecto (y sus problemas)
UUID versión 4 son 128 bits de aleatoriedad formateados como 32 caracteres hexadecimales con guiones: 550e8400-e29b-41d4-a716-446655440000. Está definido en RFC 9562 (2024, reemplazando RFC 4122). Cada lenguaje tiene un generador incluido. Cada base de datos tiene un tipo UUID. La probabilidad de colisión es astronómicamente baja — necesitarías generar 2.71 × 10^18 UUIDs para tener un 50% de probabilidad de una colisión. Para la mayoría de aplicaciones, UUIDv4 está bien.
El problema aparece a escala con índices B-tree. Como UUIDv4 es aleatorio, cada inserción va a una posición aleatoria en el índice. Esto causa divisiones de página, aumenta la amplificación de escritura y fragmenta el índice con el tiempo. En PostgreSQL con 100 millones de filas, medí 3x peor rendimiento de inserción con claves primarias UUIDv4 comparado con IDs secuenciales. El índice también era 40% más grande por la fragmentación. MySQL/InnoDB es peor porque agrupa datos por clave primaria.
UUIDv7 (también en RFC 9562) soluciona el problema de ordenamiento poniendo un timestamp Unix en los primeros 48 bits. Es básicamente lo que hace ULID pero como variante oficial de UUID. A 2026, el soporte está creciendo: PostgreSQL 17 tiene gen_random_uuid_v7(), Node.js lo tiene en el paquete uuid. Si tu ecosistema soporta UUIDv7, es lo mejor de ambos mundos — compatibilidad UUID con ordenamiento tipo ULID.
Costo de almacenamiento: un UUID son 16 bytes binarios o 36 caracteres como texto. En PostgreSQL, el tipo uuid lo almacena como 16 bytes de forma eficiente. En MySQL, guárdalo como BINARY(16) no CHAR(36) — la representación de texto desperdicia 20 bytes por fila. Con 100 millones de filas, eso son 2 GB de espacio desperdiciado solo en la columna de clave primaria.
// UUIDv4 — random, no ordering
import { v4 as uuidv4, v7 as uuidv7 } from 'uuid';
uuidv4(); // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
uuidv4(); // "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
// No relationship between them — random order
// UUIDv7 — timestamp-prefixed, sorts chronologically
uuidv7(); // "018f3e5c-9a1b-7000-8000-1a2b3c4d5e6f"
uuidv7(); // "018f3e5c-9a1c-7000-8000-7f8e9d0c1b2a"
// First 48 bits = millisecond timestamp — sorts by creation time
// Extract timestamp from UUIDv7
const id = "018f3e5c-9a1b-7000-8000-1a2b3c4d5e6f";
const ms = parseInt(id.replace(/-/g, '').slice(0, 12), 16);
new Date(ms); // 2024-05-15T10:30:00.000ZULID: ordenable, compacto y práctico
ULID (Universally Unique Lexicographically Sortable Identifier) es un ID de 128 bits con un prefijo de timestamp de 48 bits en milisegundos y 80 bits de aleatoriedad. Se codifica como 26 caracteres Crockford Base32: 01ARZ3NDEKTSV4RRFFQ69G5FAV. Se ordena lexicográficamente por tiempo de creación, lo que significa que tu índice de base de datos se mantiene secuencial y las divisiones de página son mínimas.
La diferencia de rendimiento es real. En un benchmark que corrí en PostgreSQL 15 con 50 millones de filas: las inserciones con UUIDv4 promediaron 12,000 filas/segundo con el índice creciendo a 4.2 GB. Las inserciones con ULID promediaron 31,000 filas/segundo con el índice en 2.8 GB. Eso es 2.6x más rápido en inserciones y 33% menos en tamaño de índice. La brecha se amplía conforme la tabla crece porque la fragmentación de UUIDv4 se acumula con el tiempo.
El prefijo de timestamp de ULID significa que puedes extraer el tiempo de creación sin consultar la base de datos. Parsea los primeros 10 caracteres como Crockford Base32 para obtener el timestamp en milisegundos. Esto es útil para debugging ("¿cuándo se creó este registro?"), correlación de logs y consultas por rango de tiempo directamente en la columna del ID. Nuestro timestamp-converter puede decodificar timestamps de ULID.
La desventaja: ULID no es un estándar. No hay RFC, no hay tipo nativo en bases de datos, y la calidad de las librerías varía. Lo almacenas como CHAR(26) o BINARY(16). Algunos ORMs no lo reconocen. Si necesitas interoperar con sistemas que esperan UUIDs, necesitarás funciones de conversión. UUIDv7 te da los mismos beneficios con compatibilidad UUID — pero ULID tiene 5 años de ventaja en soporte de librerías.
Snowflake IDs: cuando necesitas enteros de 64 bits
El formato Snowflake de Twitter (2010) empaqueta un timestamp, ID de máquina y número de secuencia en un entero de 64 bits. La distribución: 1 bit sin usar, 41 bits para timestamp en milisegundos (69 años desde el epoch), 10 bits para ID de máquina/datacenter (1,024 máquinas), 12 bits para secuencia (4,096 IDs por milisegundo por máquina). Discord, Instagram y Sony usan variantes de este esquema.
La ventaja sobre UUID/ULID: los Snowflake IDs son enteros planos de 64 bits. Caben en una columna bigint, se ordenan naturalmente y ocupan la mitad del almacenamiento de un UUID de 128 bits. JavaScript puede manejarlos (apenas — Number.MAX_SAFE_INTEGER es 2^53, así que necesitas BigInt o representación string para IDs arriba de 9 cuatrillones). También son más rápidos de comparar que IDs basados en strings.
La desventaja: necesitas un mecanismo de coordinación para asignar IDs de máquina únicos. Si dos servidores obtienen el mismo ID de máquina, generarán IDs que colisionan. Twitter usaba ZooKeeper para esto. Discord usa el process ID. También puedes usar los últimos 10 bits de la dirección IP del servidor. Este requisito de coordinación hace que los Snowflake IDs sean más difíciles de desplegar en entornos serverless o auto-scaling donde la identidad de la máquina es efímera.
Cuándo usar Snowflake: sistemas de alto rendimiento generando millones de IDs por segundo donde la eficiencia de almacenamiento importa (feeds de redes sociales, streams de eventos, sistemas de mensajería). Cuándo evitarlo: funciones serverless, generación de IDs del lado del cliente, o cualquier sistema donde no puedas garantizar IDs de máquina únicos. Para la mayoría de aplicaciones web, ULID o UUIDv7 es más simple y suficiente.
El problema del ordenamiento (por qué importa para bases de datos)
Los índices B-tree funcionan mejor con inserciones secuenciales. Cuando insertas una fila con una clave mayor que todas las existentes, se agrega a la página hoja más a la derecha. Sin divisiones de página, sin rebalanceo, mínima amplificación de escritura. Por eso los IDs auto-increment dan el mejor rendimiento de inserción — cada inserción va al final.
Las claves UUIDv4 aleatorias insertan en posiciones aleatorias del B-tree. Cada inserción tiene alta probabilidad de caer en una página llena, disparando una división de página (la página se divide a la mitad, la mitad de las entradas se mueven a una nueva página). Las divisiones de página son costosas: requieren asignar una nueva página, copiar datos y actualizar punteros padre. A tasas altas de inserción, esto se convierte en el cuello de botella.
ULID y UUIDv7 resuelven esto haciendo que los IDs sean monótonamente crecientes (dentro del mismo milisegundo, el sufijo aleatorio provee unicidad). Las inserciones son casi secuenciales, así que se agregan a las páginas más a la derecha. Obtienes los beneficios de distribución de IDs aleatorios (sin hotspot en un solo contador auto-increment) con el rendimiento de índice de IDs secuenciales.
Un matiz: si tienes múltiples servidores de aplicación generando IDs en el mismo milisegundo, los IDs no serán perfectamente secuenciales — serán secuenciales dentro de cada servidor pero intercalados entre servidores. Esto sigue siendo mucho mejor que totalmente aleatorio porque el intercalado ocurre dentro de una ventana de 1ms, no a través de todo el espacio de claves. Las páginas del índice para "ahora mismo" están calientes en caché sin importar qué servidor generó el ID.
Seguridad y filtración de información
Los IDs auto-increment filtran información. Si tu user ID es 48,293, un atacante sabe que tienes aproximadamente 48,293 usuarios. Si crean una cuenta y obtienen el ID 48,294, saben que nadie se registró entre sus dos solicitudes. Los competidores pueden rastrear tu tasa de crecimiento creando cuentas periódicamente. Por eso la mayoría de APIs públicas usan IDs opacos.
UUIDv4 no filtra nada — es aleatorio. ULID y UUIDv7 filtran el timestamp de creación (con precisión de milisegundos). Si esto importa depende de tu modelo de amenazas. Para un post en redes sociales, saber que fue creado el 2026-06-04T10:30:00Z probablemente está bien — el post tiene un timestamp visible de todos modos. Para un documento secreto en borrador, filtrar el tiempo de creación a través de la URL podría ser indeseable.
Los Snowflake IDs filtran tanto timestamp como ID de máquina. Los bits del ID de máquina pueden revelar la topología de tu infraestructura (cuántos servidores tienes, qué datacenter manejó la solicitud). Para sistemas internos esto está bien. Para IDs públicos, considera si esta información ayuda a los atacantes.
Si necesitas IDs verdaderamente opacos sin filtración de información, usa UUIDv4 o nanoid (un ID aleatorio más corto). Acepta el costo de rendimiento en índices o usa un ID secuencial interno separado para indexación mientras expones el ID aleatorio externamente. Muchos sistemas usan ambos: una clave primaria bigint interna para joins e índices, más un UUID público para respuestas de API y URLs.
Recomendaciones prácticas (árbol de decisión)
Base de datos única, menos de 10 millones de filas, sin sistemas distribuidos: Usa bigint auto-increment. Es simple, rápido y cada ORM lo soporta. Agrega una columna UUID pública si necesitas IDs opacos externos. No sobre-ingenieres esto para un sistema que no lo necesita.
Sistema distribuido, necesitas IDs generados en múltiples servidores sin coordinación: Usa UUIDv7 si tu ecosistema lo soporta (PostgreSQL 17+, librerías UUID modernas). Si no, usa ULID. Ambos te dan ordenamiento por timestamp y probabilidad de colisión despreciable sin ningún servicio de coordinación. Genera IDs del lado del cliente o del servidor — no importa.
Sistema de alto rendimiento (>100K inserciones/segundo), sensible al almacenamiento: Usa Snowflake IDs si puedes manejar la asignación de IDs de máquina. El tamaño de 64 bits reduce a la mitad el almacenamiento de índices comparado con UUIDs de 128 bits. El límite de 4,096 IDs por milisegundo por máquina rara vez es un problema en la práctica.
Necesitas máxima compatibilidad con sistemas existentes: Usa UUIDv4. Cada base de datos, cada ORM, cada framework de API entiende UUIDs. La penalización de rendimiento solo importa a escala (decenas de millones de filas con cargas pesadas de escritura). Para cargas de lectura intensiva o tablas más pequeñas, UUIDv4 funciona perfectamente. Nuestro uuid-generator crea variantes v4 y v7 para pruebas.
Estrategias de migración (cuando elegiste mal)
Si estás migrando de auto-increment a UUID/ULID (común al mover a microservicios o necesitar generación de IDs del lado del cliente): agrega la nueva columna de ID, rellénala para filas existentes, actualiza todas las claves foráneas y código de aplicación para usar la nueva columna, luego elimina la columna vieja. Haz esto en etapas — no intentes una migración big-bang en una base de datos de producción.
Si estás migrando de UUIDv4 a ULID/UUIDv7 por rendimiento: no puedes simplemente re-codificar IDs existentes porque tendrían timestamps aleatorios. Opciones: mantener filas existentes con sus IDs UUIDv4 y solo usar ULID para filas nuevas (el índice gradualmente se volverá más secuencial conforme se eliminen filas viejas), o hacer una reescritura completa con nuevos IDs (requiere actualizar todas las referencias).
El enfoque más seguro para cualquier migración de IDs: escritura dual durante un período de transición. Escribe ambos IDs viejo y nuevo, sirve ambos en APIs (acepta cualquiera en búsquedas), luego haz el corte una vez que todos los clientes usen el nuevo formato. Esto evita downtime pero duplica tu almacenamiento de IDs temporalmente.
Algo que he aprendido de tres migraciones de IDs: la parte más difícil no es la base de datos — es encontrar cada lugar donde aparece el ID. Archivos de log, eventos de analytics, integraciones con partners externos, respuestas cacheadas, colas de mensajes, servicios de tracking de errores. Mapea todos los consumidores antes de empezar. La migración que "debería tomar una semana" siempre toma un mes por referencias olvidadas en sistemas que no controlas.