UUID, ULID, NanoID e CUID2: quale ID usare nel 2026?

Il problema degli ID nei sistemi moderni

Ogni volta che progetti un sistema, prima o poi ti scontri con la stessa domanda: come identifico in modo univoco le mie entità? L’ID auto-increment del database va bene per un sito piccolo, ma nel momento in cui hai più istanze, repliche, o devi generare ID lato client, il gioco si complica.

I problemi concreti con gli ID sequenziali sono tre. Prima cosa: rivelano informazioni — se il tuo ordine è ORDER-00042, un utente malizioso sa quanti ordini hai. Seconda cosa: non puoi generarli senza passare dal database, il che crea un collo di bottiglia in sistemi distribuiti. Terza cosa: in ambienti multi-tenant o con sharding, i conflitti di ID sono un rischio reale.

Ed è qui che entrano in gioco UUID, ULID e compagnia.

Contesto

Tutti i formati che vedremo producono ID globalmente univoci senza coordinamento centrale. Questo significa che puoi generarli in qualsiasi processo, in qualsiasi momento, senza rischio di collisioni — o quasi.


UUID — il classico intramontabile

UUID sta per Universally Unique Identifier. Esiste dagli anni ‘80 ed è standardizzato come RFC 4122. È un numero da 128 bit rappresentato come stringa esadecimale con trattini:

550e8400-e29b-41d4-a716-446655440000

Ne esistono diverse versioni. Le tre che incontri nella pratica sono v1, v4 e la più recente v7.

UUID v4 — il più diffuso

UUID v4 è puramente casuale. 122 bit di casualità (i restanti 6 bit sono riservati alla specifica del formato). La probabilità di collisione è astronomicamente bassa: dovresti generare circa un miliardo di UUID al secondo per 85 anni prima di avere un 50% di probabilità di collisione. In pratica, non ci pensi.

In Node.js, dalla versione 14.17, non hai bisogno di librerie esterne:

// Node.js nativo — nessuna dipendenza
import { randomUUID } from 'node:crypto';

const id = randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"

// Oppure con la libreria uuid, se preferisci la flessibilità
import { v4 as uuidv4, v1 as uuidv1, v7 as uuidv7 } from 'uuid';

const idV4 = uuidv4(); // casuale
const idV1 = uuidv1(); // basato su timestamp (deprecato di fatto)
const idV7 = uuidv7(); // timestamp + casualità (il futuro)

UUID v7 — il futuro (già presente) ordinabile

UUID v7 è stato standardizzato nella RFC 9562 del 2024. Risolve il problema più grande di v4: l’ordinamento. I primi 48 bit contengono un timestamp Unix in millisecondi, il che significa che due UUID v7 generati in sequenza sono ordinabili cronologicamente. Questo fa una differenza enorme sulle performance di scrittura nei database con indici B-Tree (tipo PostgreSQL o MySQL), perché gli inserimenti non causano la frammentazione degli indici.

import { v7 as uuidv7 } from 'uuid';

// Generiamo due ID in rapida sequenza
const id1 = uuidv7();
const id2 = uuidv7();

console.log(id1); // "018e6a3f-5c00-7a1b-b2c3-d479f47ac10b"
console.log(id2); // "018e6a3f-5c01-7d2e-a3b4-e589f58bd21c"

// L'ordinamento lessicografico riflette l'ordine di creazione
console.log(id1 < id2); // true ✓

// Puoi estrarre il timestamp dall'ID
function extractTimestampFromUUIDv7(uuid: string): Date {
  const hex = uuid.replace(/-/g, '').slice(0, 12);
  const ms = parseInt(hex, 16);
  return new Date(ms);
}

Opinione personale

Se stai partendo da zero oggi e scegli di usare UUID, usa direttamente v7. Il supporto nelle librerie c’è già, i database lo gestiscono come qualsiasi UUID, e ti risparmi i mal di testa dell’ordinamento non deterministico che ti dà v4 nelle query con ORDER BY created_at.


ULID — quando l’ordine conta

ULID sta per Universally Unique Lexicographically Sortable Identifier. È stato progettato proprio per risolvere il problema che UUID v4 ignora: l’ordinamento.

01ARZ3NDEKTSV4RRFFQ69G5FAV

Un ULID è composto da 128 bit totali: i primi 48 bit sono un timestamp in millisecondi, gli ultimi 80 bit sono casuali. Il risultato è una stringa di 26 caratteri in codifica Crockford Base32 (alfabeto senza le lettere I, L, O, U che si confondono facilmente).

I vantaggi rispetto a UUID sono due. Primo: è lexicographically sortable — puoi ordinare ULID come stringhe e ottieni l’ordine cronologico. Secondo: è più compatto — 26 caratteri contro 36 di UUID.

import { ulid, decodeTime } from 'ulid';
// npm install ulid

// Generazione base
const id = ulid();
// "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// Puoi anche passare un timestamp specifico
const idConTimestamp = ulid(Date.now());

// Estrarre il timestamp è immediato
const timestamp = decodeTime(id);
console.log(new Date(timestamp));
// 2025-01-15T10:30:00.000Z

// Ordinamento lessicografico = ordinamento cronologico
const ids = [ulid(), ulid(), ulid()];
const sorted = [...ids].sort();
// sorted è già nell'ordine corretto di creazione ✓

ULID e i database: attenzione a come lo salvi

Qui c’è un dettaglio che spesso si trascura. Se salvi ULID come stringa su PostgreSQL o MySQL, perdi il vantaggio dell’ordinamento a livello di indice B-Tree (che opera in modo efficiente su tipi nativi). Il modo migliore è salvare ULID come bytea su PostgreSQL o come binary(16) su MySQL. Alcune ORM come Prisma o TypeORM hanno supporto nativo o plugin per questo.

import { ulid, decodeTime } from 'ulid';

// Conversione ULID ↔ Buffer per storage efficiente
function ulidToBuffer(id: string): Buffer {
  const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
  let value = 0n;
  for (const char of id.toUpperCase()) {
    value = value * 32n + BigInt(ENCODING.indexOf(char));
  }
  const buf = Buffer.alloc(16);
  for (let i = 15; i >= 0; i--) {
    buf[i] = Number(value & 0xffn);
    value >>= 8n;
  }
  return buf;
}

// In Prisma, con campo @db.Binary(16)
// await prisma.user.create({ data: { id: ulidToBuffer(ulid()) } })

Attenzione

ULID genera al massimo 1000 ID per millisecondo con casualità garantita. Se devi generare più ID nello stesso millisecondo, la parte random viene incrementata — ma se superi 2^80 in un millisecondo (impossibile nella pratica), torni al punto zero. Non è un problema reale, ma vale saperlo.


NanoID — compatto e veloce

NanoID non è un “formato standard” come UUID o ULID. È una libreria JavaScript/TypeScript che genera ID casuali con un alfabeto e una lunghezza configurabili. Il default produce stringhe di 21 caratteri usando l’alfabeto URL-safe:

V1StGXR8_Z5jdHi6B-myT

La libreria è minuscola (meno di 130 byte minificata + gzippata), usa crypto.getRandomValues per la casualità crittografica, e funziona sia in Node.js che nel browser senza modifiche.

import { nanoid, customAlphabet } from 'nanoid';
// npm install nanoid

// Uso base — 21 caratteri URL-safe
const id = nanoid();
// "V1StGXR8_Z5jdHi6B-myT"

// Lunghezza personalizzata
const shortId = nanoid(10);
// "IRFa-VaY2b"

// Alfabeto personalizzato — utile per codici leggibili dagli utenti
const numericOnly = customAlphabet('0123456789', 8);
const orderCode = numericOnly();
// "47291083"

// Codice di conferma senza caratteri ambigui (0/O, 1/l/I)
const safeCode = customAlphabet(
  '23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
  8
);
console.log(safeCode()); // "A3KM9P7R"

// Calcolo probabilità collisione con la libreria ufficiale:
// https://zelark.github.io/nano-id-cc/
// Con 21 char e alfabeto 64: ~149 anni a 1000 ID/sec per avere 1% di probabilità

Quando ha senso usare NanoID

NanoID brilla nei casi in cui hai bisogno di ID corti e leggibili — codici promozionali, token di sessione, link abbreviati, ID esposti nelle URL pubbliche. Non è pensato come primary key di database (manca di ordinabilità e non ha una struttura standardizzata), ma per questi casi d’uso specifici è imbattibile.

Opinione personale

NanoID lo trovo personalmente ottimo quando devo esporre un ID nelle URL o mostrarlo all’utente. La possibilità di definire l’alfabeto è killer: posso togliere i caratteri ambigui (0, O, 1, I, l) e ottenere codici che l’utente riesce a digitare a mano senza bestemmiare.


CUID2 — pensato per i database

CUID (Collision-resistant Unique ID) è arrivato con una promessa precisa: essere ottimizzato per database e sistemi orizzontalmente scalabili. La versione 2 — CUID2 — è una riscrittura completa del 2022 che ha abbandonato il prefisso fisso e migliorato la casualità.

clh3eke620000356ok53fgx3e

Un CUID2 è sempre in minuscolo, lungo 24 caratteri per default (configurabile), inizia sempre con una lettera (utile per usarlo come nome di variabile o classe CSS), e include un fingerprint della macchina per ridurre ulteriormente le collisioni in ambienti distribuiti.

import { createId, init, isCuid } from '@paralleldrive/cuid2';
// npm install @paralleldrive/cuid2

// Uso base
const id = createId();
// "clh3eke620000356ok53fgx3e"

// Configurazione personalizzata
const createShortId = init({
  length: 16,
  // fingerprint personalizzato per istanza del servizio
  fingerprint: process.env.SERVICE_NAME ?? 'default',
});

const shortId = createShortId();
// "c8k2nm4pq9rst6uv"

// Validazione — utile per sanitizzare input da API
function validateUserId(id: unknown): string {
  if (typeof id !== 'string' || !isCuid(id)) {
    throw new Error('ID utente non valido');
  }
  return id;
}

Suggerimento

CUID2 include di default un “fingerprint” basato sul processo corrente, che aiuta a ridurre collisioni su sistemi multi-processo senza coordinamento. Puoi sovrascriverlo con un identificatore del servizio per rendere il sistema ancora più prevedibile.

CUID2 con Prisma

Prisma supporta CUID2 nativamente come generatore di ID nel schema.prisma:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}

// Per CUID2 specificamente con Prisma 5+:
model Post {
  id String @id @default(cuid(2))
}

Confronto diretto

Metto tutto su un’unica tabella perché a un certo punto vuoi semplicemente vedere i numeri:

FormatoLunghezzaOrdinabileTimestampStandardBrowserCaso d’uso principale
UUIDv436 charRFC 4122
UUIDv736 char (ms)RFC 9562
ULID26 char (ms)Spec apertaEvent sourcing, log, timeseries
NanoID21 char*URL, token, codici utente
CUID224 char*Database, API, ORM
  • lunghezza configurabile

Un benchmark di generazione

I numeri variano da macchina a macchina, ma le proporzioni relative sono abbastanza stabili. Ho misurato su Node.js 22, generando 1.000.000 di ID per tipo:

randomUUID() → ~9.8M op/s
uuid v4      → ~7.2M
uuid v7      → ~6.8M
ulid         → ~4.1M
nanoid       → ~3.9M
cuid2        → ~1.2M

CUID2 è più lento perché usa SHA-3 internamente per rafforzare la casualità. Ma considera che in un endpoint HTTP il collo di bottiglia è la query al database, non la generazione dell’ID.


Pro e contro riassunti

UUID v4

Pro

  • Supporto nativo in tutti i database moderni
  • Standard RFC — massima interoperabilità
  • Nessuna dipendenza in Node.js 14.17+
  • Tooling maturo e ubiquo
  • Validazione regex semplice

Contro

  • Non ordinabile — frammentazione degli indici B-Tree
  • 36 caratteri — verboso nelle URL e nei log
  • Nessuna informazione temporale embedded
  • Leggibilità umana limitata

UUID v7

Pro

  • Ordinabile cronologicamente
  • Standard RFC 9562 — già supportato
  • Compatibile con colonne UUID esistenti
  • Timestamp estraibile senza query aggiuntive
  • Migliori performance su indici B-Tree

Contro

  • Ancora 36 caratteri con trattini
  • Supporto nativo nei DB ancora non uniforme
  • Rivela (parzialmente) il momento di creazione

ULID

Pro

  • Ordinabile lessicograficamente
  • Compatto — 26 char vs 36 di UUID
  • Alfabeto Crockford senza caratteri ambigui
  • Timestamp embedded con ms precision
  • Ottimo per event sourcing e log

Contro

  • Non uno standard formale (RFC)
  • Supporto nativo nei DB quasi assente
  • Richiede attenzione per storage efficiente
  • Rivela timestamp di creazione

NanoID

Pro

  • Compatto e personalizzabile
  • Funziona nel browser senza modifiche
  • Alfabeto configurabile — perfetto per codici leggibili
  • Bundle size minuscolo (<130 byte gz)
  • Casualità crittograficamente sicura

Contro

  • Nessun ordinamento temporale
  • Nessuno standard — validazione custom
  • Non adatto come primary key DB
  • Nessun timestamp embedded

CUID2

Pro

  • Ottimizzato per database e sharding
  • Fingerprint di processo riduce collisioni
  • Inizia sempre con lettera — HTML/CSS safe
  • Lunghezza configurabile
  • Integrazione nativa con Prisma

Contro

  • Il più lento nella generazione
  • Nessuno standard formale
  • Nessun ordinamento temporale
  • Dipendenza esterna necessaria

Cosa uso io di solito e perché

  • Per le primary key di database uso UUID v7. Il motivo è semplice: è uno standard aperto, i database lo gestiscono nativamente come tipo UUID, e il vantaggio dell’ordinamento sugli indici è concreto — in tabelle sopra al milione di righe si nota. UUID v4 lo considero sostanzialmente superato per i nuovi progetti.

  • Per gli ID esposti nelle URL pubbliche o nei token uso NanoID, tipicamente con un alfabeto personalizzato che esclude i caratteri ambigui. 21 caratteri sono sufficienti per la casualità, e la compattezza fa differenza quando l’ID finisce in un log o in un link condiviso.

  • Per i sistemi event-sourcing o dove ho bisogno di ordinamento e timestamp senza ricorrere al database, uso ULID. È il formato più elegante del gruppo per questa categoria di problemi.

  • CUID2 lo uso principalmente quando lavoro con Prisma su progetti dove l’integrazione nativa vale la semplicità. Non è la mia prima scelta fuori da quel contesto.

Un albero decisionale rapido:

*Hai bisogno di una primary key in un database relazionale?*
→ UUID v7 (o CUID2 se usi Prisma)

*Hai bisogno di un ID nelle URL o visibile all'utente?*
→ NanoID con alfabeto personalizzato

*Stai costruendo event sourcing o hai bisogno di ordinamento temporale senza DB?*
→ ULID

*Devi integrarti con sistemi terzi che parlano UUID?*
→ UUID v4 (per compatibilità) o v7 (se lo supportano)

Non esiste la scelta “universalmente corretta”. Esiste la scelta giusta per il tuo contesto. Se stai iniziando un progetto nuovo nel 2025, UUID v7 copre il 90% dei casi d’uso senza farti perdere tempo. Parti da lì e cambia solo quando hai un motivo concreto.

Librerie di riferimento

# UUID — tutte le versioni inclusa v7
npm install uuid
npm install --save-dev @types/uuid

# ULID
npm install ulid

# NanoID (ESM nativo dalla v4)
npm install nanoid

# CUID2
npm install @paralleldrive/cuid2

# UUID v4 nativo (no dipendenze, Node.js 14.17+)
import { randomUUID } from 'node:crypto';

Tip finale

Parti da UUID v7 e cambia solo se necessario.