UUID vs ULID vs nanoid: picking an identifier
Every system that creates records needs unique identifiers, and the "just use a UUID" default is not always the right one anymore.
UUID v4: the default
A UUID v4 is 128 bits, almost entirely random (six bits are fixed to identify the version and variant). Formatted as 36 characters including hyphens:
f47ac10b-58cc-4372-a567-0e02b2c3d479The collision probability is negligible at any realistic scale — you would need to generate roughly a billion UUIDs per second for a thousand years before a 50% chance of one collision. The downside is that random UUIDs are not sortable by creation time and are poor primary keys for databases using B-tree indexes, because inserting random values causes constant index page splits and fragmentation instead of appending to the end.
UUID v7: time-ordered UUIDs
A newer standard (finalized in 2024) that packs a Unix millisecond timestamp into the leading bits and fills the rest with randomness, keeping the same 128-bit, 36-character format as v4 but making IDs generated later sort after IDs generated earlier. This fixes the index fragmentation problem while remaining a drop-in UUID wherever v4 was used, and is now the recommended default for new systems where sortable IDs help.
ULID: sortable and more compact to type
A ULID is also 128 bits (48-bit timestamp + 80-bit randomness) but encoded in Crockford's base32, giving a 26-character, case-insensitive, URL-safe string:
01ARZ3NDEKTSV4RRFFQ69G5FAVLike UUID v7, ULIDs sort lexicographically by creation time, which means they double as a rough "created at" indicator and behave well as database primary keys. They predate UUID v7's standardization and remain popular for the same reasons, with the tradeoff that they are not RFC 4122 UUIDs, so a strict UUID column type or validator will reject them.
nanoid: small, fast, customizable
nanoid generates a short random string (21 characters by default) from a URL-safe alphabet, using a cryptographically secure random source. It is not time-sortable and carries no structure — it is purely a compact, collision-resistant random token:
V1StGXR8_Z5jdHi6B-myTIts main advantages are size (21 characters vs 36 for a UUID) and a configurable alphabet/length, which makes it a good fit for things like short public share links, invite codes and session tokens where a canonical UUID format is unnecessary and the shorter string genuinely helps (URLs, QR codes, manual entry).
Collision risk in practice
All three are designed so that collisions are astronomically unlikely for their default sizes, but shrinking the alphabet or length (a common request for "short codes") trades away that safety margin quickly — nanoid's own collision calculator shows that dropping to 10 characters at the default alphabet still needs about 17 years at 1000 IDs/second for a 1% collision probability, but going shorter still, or reducing the character set (letters only, no symbols), erodes that fast.
Choosing
- **Random UUID v4** — safe default when interoperability with existing UUID columns/APIs matters and sort order is irrelevant.
- **UUID v7** — new systems that want time-ordering and standard UUID compatibility simultaneously.
- **ULID** — same time-ordering benefit, more compact string, when you are not constrained to the UUID format specifically.
- **nanoid** — short-lived or user-facing tokens (links, codes) where a compact, URL-safe string matters more than structure.
Validating what you receive
If your system accepts an identifier from an external source, validate its format before using it in a query — a malformed "UUID" that is actually attacker-controlled input is a a real injection surface if it reaches a raw SQL string instead of a parameterized query, independent of whether the ID format itself is secure.