Generators

UUID vs NanoID vs ULID: Which ID Format Should You Use

UUIDv4, NanoID, and ULID all generate unique IDs but optimize for different things. Here's the actual tradeoffs — length, sortability, collision risk, and database performance.

Nikhil Sai··5 min read
All posts

Use UUIDv4 for maximum compatibility and tooling support, NanoID when you want a shorter URL-friendly ID with configurable length, and ULID when you need IDs that sort chronologically — which matters a lot for database index performance. They're not interchangeable; picking wrong shows up later as either bloated indexes or ugly URLs.

What each one actually is

UUIDv4 — 128 bits of randomness, formatted as xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 characters including hyphens). The version 4 variant is fully random except for a few fixed bits that mark it as v4. It's an IETF standard (RFC 4122), which means it's supported natively by essentially every database, language, and ORM without a library.

NanoID — a shorter, URL-safe random ID generator. Default output is 21 characters from a 64-character alphabet (A-Za-z0-9_-), tuned so collision probability stays comparably low to UUIDv4 despite being nearly half the length. Length and alphabet are both configurable, which UUID's fixed format doesn't allow.

ULID — Universally Unique Lexicographically Sortable Identifier. 26 characters, Crockford's Base32 encoded, structured as a 48-bit timestamp (millisecond precision) followed by 80 bits of randomness. The timestamp prefix means ULIDs generated later always sort after ones generated earlier — as strings, without needing a separate created_at column to order by.

Side by side

| | UUIDv4 | NanoID | ULID | |---|---|---|---| | Length | 36 chars | 21 chars (default, configurable) | 26 chars | | Sortable by creation time | No | No | Yes | | URL-safe by default | No (hyphens are fine, but not compact) | Yes | Yes | | Standard/RFC | Yes (RFC 4122) | No (library convention) | No (spec, not RFC) | | Random bits | 122 | ~126 (at default length) | 80 | | Database index friendliness | Poor (random insert order fragments B-tree indexes) | Poor (same issue) | Good (sequential-ish inserts) | | Native DB/language support | Universal | Requires the library | Requires the library |

Why sortability matters more than it sounds like it should

This is the part that actually affects production systems, not just aesthetics. Most databases store primary keys in a B-tree index, and B-trees perform best with roughly sequential inserts — new rows get appended near the end of the index rather than scattered randomly throughout it.

A random primary key (UUIDv4, NanoID) means every insert lands at a random point in the index, which causes page splits, fragmentation, and worse cache locality as the table grows. This is a well-documented problem with UUID primary keys at scale — it's not theoretical, it shows up as degraded write throughput and bloated indexes on large tables.

ULID solves this by putting a timestamp first: IDs generated close together in time are also close together lexicographically, so inserts stay roughly sequential even though the ID itself is still globally unique and unguessable in the random portion. You get UUID-like uniqueness with B-tree-friendly insert patterns.

Collision risk in practice

All three are designed so collision probability is negligible at realistic scale — this isn't really a differentiator in practice, but the math differs:

  • UUIDv4: 122 random bits. You'd need roughly 2.71 quintillion UUIDs generated before a 50% chance of one collision.
  • NanoID at 21 chars: ~126 bits of entropy from its 64-character alphabet — comparable to or slightly better than UUIDv4 despite being shorter, because it uses a larger alphabet per character.
  • ULID: only 80 random bits (the other 48 are the timestamp). Still enormous — about 1.2 x 10^24 unique IDs per millisecond before meaningful collision risk — but worth knowing it's a smaller random space than the other two if you're generating extreme volumes within the same millisecond.

When to use which

UUIDv4 — default choice when you want zero dependencies and maximum compatibility: public APIs, database primary keys where you're not at a scale where index fragmentation matters yet, anywhere a UUID is explicitly expected.

NanoID — user-facing IDs in URLs (/share/V1StGXR8_Z5jdHi6B-myT), short codes, session tokens where UUID's 36 characters feel unnecessarily long and the hyphens look unpolished in a URL bar.

ULID — high-write-volume tables where insert performance and index health matter (event logs, orders, time-series data), or anywhere you want IDs that double as a rough timestamp without a separate column.

Quick decision guide

  • Need it to look/parse like a standard, work everywhere with zero setup → UUIDv4
  • Need it short and clean in a URL, length is configurable to your needs → NanoID
  • Need chronological sort order and are optimizing database write performance → ULID
  • Not sure and don't have a scale problem yet → UUIDv4, it's the safest default

Generating any of these by hand or with inconsistent library versions across services is a good way to end up with subtly incompatible ID formats. A UUID Generator, NanoID Generator, and ULID Generator each produce spec-correct IDs instantly, client-side — useful for quick testing, seeding fixtures, or just checking what a given format actually looks like before committing to it in a schema.

Related posts