Use the NanoID Generator to inspect compact IDs, but choose UUID or ULID when interoperability, ordering, or database index behavior matters more than length. Every production path still needs a uniqueness constraint.
Identifier formats look interchangeable until they enter URLs, database indexes, logs, and security boundaries. NanoID, UUID, and ULID optimize different properties: compatibility, length, time ordering, or human-facing output, and the differences are measurable rather than aesthetic. A UUIDv4 carries 122 random bits in 36 characters. A default NanoID carries 126 bits in 21. A ULID spends 48 of its 128 bits on a millisecond timestamp and keeps 80 for randomness, which makes it sortable and slightly weaker in the pure collision sense. This guide attaches numbers to those trade-offs so the choice still holds when the same identifier shows up in a primary key, a support ticket, and an analytics export. Choose from the behavior your system needs, not from the shortest string.
1. The short answer
| Need | Good default | Why |
|---|---|---|
| Standard library compatibility | UUID | Widely supported and familiar |
| Compact URL-safe public IDs | NanoID | Short configurable alphabet |
| Time-sortable distributed IDs | ULID | Timestamp prefix plus randomness |
| Password or reset secret | Dedicated random secret | An ID is not an authorization policy |
UUID is the interoperability choice: parsers, ORMs, and message schemas already understand it. NanoID is useful when a short URL token is easier to handle than a textual UUID. ULID helps with chronological sorting, but its timestamp can reveal creation time, and any format that embeds metadata leaks that metadata to everyone who can read an identifier. What the table cannot capture is scale: an identifier that is comfortable at ten thousand rows becomes a liability at one billion, and one that sorts correctly inside a single process can sort incorrectly across twelve writers. The next sections make those thresholds explicit.
2. How the formats differ
UUIDs are commonly written as 32 hexadecimal digits with hyphens. UUIDv4 is random; other versions have different properties. NanoID uses a URL-friendly alphabet and configurable size. ULID encodes a 48-bit millisecond timestamp followed by 80 bits of randomness.
UUID: 550e8400-e29b-41d4-a716-446655440000
NanoID: V1StGXR8_Z5jdHi6B-myT
ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAVRead the structures literally, because the spelling of each value encodes a design decision:
- UUIDv4: 128 bits displayed as 32 hex digits; 6 bits are reserved for the version and variant fields, leaving 122 random bits. Hyphens are display sugar — the underlying value is 16 bytes.
- NanoID: a sequence of characters drawn from an alphabet you choose. The default is 21 characters over a 64-character set (A–Z, a–z, 0–9, underscore, hyphen). Entropy in bits equals size times log2 of the alphabet size, so the default yields 21 × 6 = 126 bits.
- ULID: 48 bits of big-endian millisecond timestamp plus 80 random bits, rendered as 26 characters of Crockford base32. The first 10 characters are time; the last 16 are randomness.
These examples show representation, not a security guarantee. A value is only as strong as its random source, generation process, and access controls. A NanoID drawn from Math.random() inherits none of the entropy math above.
3. Entropy and collision math
Collision risk is not a vibe; it follows from the birthday bound. For an identifier space of b bits and n generated values, the probability of at least one collision is approximately n² / 2^(b+1), and the 50 percent threshold sits at roughly 1.177 × 2^(b/2) values. Working that formula for each format makes the practical limits concrete:
| Format | Text length | Random bits | 50% collision threshold |
|---|---|---|---|
| UUIDv4 | 36 chars | 122 | About 1.3 × 10^18 values |
| NanoID, default | 21 chars | 126 | About 1.3 × 10^19 values |
| NanoID, size 12 | 12 chars | 72 | About 6.8 × 10^10 values |
| ULID random tail | 26 chars | 80 | About 1.3 × 10^12 values in one millisecond |
| 4-digit order code | 4 chars | About 13 | About 180 values |
Two readings matter. First, UUIDv4 and default NanoID are both far beyond any realistic row count; picking between them on collision grounds is superstition. Second, ULID randomness applies per generation moment: two writers minting ULIDs in the same millisecond share only the 80-bit tail, so the relevant comparison is trillions of same-millisecond identifiers, not trillions of lifetime identifiers. That is still enormous, which is why a monotonic increment within a millisecond is a cheap safety upgrade.
Shortening changes the picture fast. A 12-character NanoID is fine for a few billion rows and uncomfortable past that; an 8-character value over the same alphabet (48 bits) crosses 50 percent near 200 million. If you shorten for UX, decide the maximum lifetime volume first, then pick the smallest length whose threshold exceeds it by orders of magnitude.
Probability is still not a substitute for a uniqueness constraint. Collisions are handled by regenerating and reinserting: catch the constraint violation, mint a fresh value, try again. The retry path is cheap precisely because genuine collisions are rare; the constraint is what turns a theoretical risk into a routine error branch.
4. Database and index behavior
Random identifiers can fragment clustered indexes because inserts arrive at arbitrary positions. A B-tree in PostgreSQL defaults to 8 KB pages and MySQL InnoDB to 16 KB, and each page holds one slice of the key space. Monotonic keys fill the rightmost page until it splits. Random UUID keys touch a different region on nearly every insert, so the tree splits pages across its whole width, leaves them half-full, and forces reads to visit more of them. Bulk loads with random UUID primary keys are routinely several times slower than the same load against a sequential key, and the index itself grows because of the dead space. Time-oriented identifiers can improve locality for append-heavy workloads, but benchmark with your database rather than trusting a blog table.
Storage compounds the effect. PostgreSQL's native uuid type is 16 bytes, the hyphenated text form is 36 bytes, a ULID is 26 characters of text or 16 bytes if you pack it, and a default NanoID is 21 characters of text. Every secondary index that includes the key duplicates that cost, and every foreign key replicates it again on the referencing table. Longer text keys also make comparisons more expensive once they pass the point where a CPU word holds the whole value.
Many databases provide native or compact UUID types, and newer versions ship time-ordered helpers: PostgreSQL offers gen_random_uuid() and, in recent releases, uuidv7(), while MySQL can store UUIDs in a 16-byte binary form with a byte-swap option so time-ordered versions insert compactly. ULID can be stored as binary or text with the same trade-off. NanoID reduces URL length but still has a storage cost. Consistency across services matters more than the spelling you prefer.
Never expose a sequential internal ID merely to save bytes. Predictable identifiers enable enumeration unless authorization is independently enforced. The common compromise is a private 64-bit surrogate for joins plus a separate unique public identifier for URLs: the database keeps its efficient key and the API never leaks row order.
5. UUID versions worth knowing
"UUID" is a family, not a single format, and RFC 9562, finalized in 2024, expanded the useful members. The versions you will actually meet:
| Version | Layout | What it is good for |
|---|---|---|
| v1 | 60-bit timestamp + 48-bit MAC address | Legacy systems; leaks hardware address and creation time |
| v3 | MD5(namespace, name) | Deterministic labels for stable objects; not a security primitive |
| v4 | 122 random bits | The default "random UUID" in every standard library |
| v5 | SHA-1(namespace, name) | Deterministic labels where v3's hash is considered too weak |
| v6 | Reordered v1 fields | Time-ordered hex identifiers for systems migrating from v1 |
| v7 | 48-bit millisecond timestamp + random bits | Time-ordered database keys — the modern default for new schemas |
| v8 | Custom layout with reserved bits | Vendor-specific formats that must still look like UUIDs |
Name-based versions (v3, v5) are deterministic: the same name in the same namespace always yields the same UUID. That is useful for stable identifiers derived from natural keys, and useless as a secret, because anyone who knows the name can compute the value. For random identifiers, v4 remains the compatibility baseline and v7 is the version to reach for when you want insertion locality without adopting ULID's base32 encoding. A ULID is structurally close to a v7 wearing a different alphabet plus a spec-level monotonicity convention.
6. Randomness and security
Use a cryptographically secure random source for identifiers that protect access to a resource. JavaScript Math.random() is not suitable. Standard CSPRNG entry points are crypto.randomUUID() in Node and modern browsers, crypto.getRandomValues() for raw bytes, the secrets module in Python, crypto/rand in Go, and SecureRandom in Java. A local browser tool can create samples with crypto.getRandomValues(), but production services should use the secure API for their runtime.
Separate two jobs that share the word "ID". A row key only needs enough entropy to avoid accidents, and its collision budget is the birthday math from section 3. A capability — a password reset link, an API key, an invite code — must additionally resist an attacker who can try guesses, so treat it like a secret: at least 128 bits of CSPRNG output, a short expiry, single use, and only a hash at rest (the Hash Generator covers the usual encodings for storing one). The identifier's unpredictability is doing authorization work there; for a plain row key it is not.
ULID's timestamp is useful for sorting but can reveal creation time. If that metadata is sensitive, use a random identifier or separate the public token from internal timestamps.
7. Sortability and index locality
Lexical order matters because databases sort text keys bytewise. Crockford base32, which ULID uses, lists digits before letters in the same order as ASCII, so sorting ULID strings bytewise equals sorting them by value; hex behaves the same way when case is consistent. That alignment is the entire reason time-prefixed formats exist: an ORDER BY id on a ULID column approximates ORDER BY created_at without a second index, and new rows land at the right edge of the B-tree instead of scattering across it.
Three caveats keep "sortable" honest. The ULID timestamp has millisecond resolution, so events inside one millisecond are ordered only by the random tail unless the generator is monotonic — a monotonic factory increments the random part instead of redrawing it, which preserves order within one process. Cross-process order is approximate: two writers with clock skew will interleave, and an NTP step can push a slower clock's identifiers behind faster ones for a while. And sortability is a convenience, not a correctness mechanism; if your logic depends on a total order across writers, add an explicit sequence or transaction timestamp rather than trusting identifier string order alone.
8. Alphabets, URL safety, and encoding
Each format's alphabet determines URL behavior, transcription risk, and how validators must be written. UUID hex plus hyphens needs no escaping in paths or queries. The default NanoID alphabet — letters, digits, underscore, hyphen — is URL-safe and filesystem-friendly, so values drop into a path segment without percent-encoding. Crockford base32 deliberately omits I, L, O, and U so a human reading an identifier aloud cannot confuse 1 with l or 0 with O.
Other encodings are worse neighbors. base64 uses +, /, and =, all of which force escaping or replacement inside a URL. base58, familiar from cryptocurrency addresses, removes visually ambiguous characters for the same reason Crockford base32 does; the trade-offs between the two families are covered in base58 vs base64, and the base58 encoder makes the length difference tangible — base58 packs about 5.86 bits per character against base64's 6, so shorter always costs more total characters for the same entropy.
Case sensitivity is the quiet trap. NanoID values are case-sensitive — V1St and v1st are different identifiers — so any layer that lowercases URLs, any case-insensitive collation on the key column, and any user pasting through a careless tool can break lookups. UUID hex sidesteps this with a lowercase convention; Crockford base32 sidesteps it by being uppercase-only. Whatever you pick, lock the alphabet, the case rules, and the validator regex in one place, and never change the alphabet after launch: every stored value and every pattern-matching rule in production was written against the old one.
9. Distributed and multi-writer generation
The defining requirement of a distributed identifier is generating without coordination. A service that must ask a central authority for the next number has added a network hop, a failure mode, and a serialization point to every write path. Random formats satisfy the requirement trivially: any node with a CSPRNG can mint identifiers independently. Time-prefixed formats satisfy it too, because uniqueness comes from the random tail rather than from a shared counter.
Ordered schemes that do use counters pay for the order elsewhere. Snowflake-style identifiers split the bits between millisecond time, a worker identifier, and a per-worker sequence — classically 41, 10, and 12 bits — which caps the system at a fixed number of workers and misbehaves when clocks roll backwards. Ticket servers or range allocation, where each node takes a block of integers, restore simple numbers but reintroduce a central component. ULID avoids the worker-identifier problem entirely: ordering is emergent from clocks rather than provisioned from a bit budget, at the cost of the approximate cross-writer ordering described above.
Migration is the last distributed concern. Formats are hard to change once identifiers appear in URLs, foreign keys, and client caches. The pattern that works: keep parsing both formats in readers, mint new identifiers in the new format, and backfill old rows only when the benefit justifies the write volume. A unique constraint that accepts either representation, plus a documented canonical form, prevents the migration from becoming a second identifier system that lives forever.
10. Decision guide
- Short public links: NanoID with documented size and alphabet.
- Cross-language events: UUID when consumers parse it natively.
- Log correlation with approximate order: ULID if timestamp disclosure is acceptable.
- Password reset: high-entropy, expiring, single-use token; store only a hash.
- Sortable database key: benchmark the database's recommended UUID or time-ordered format.
Condensed into a table, with the configuration each pick implies:
| Scenario | Pick | Configuration notes |
|---|---|---|
| Public resource URLs | NanoID | 21+ characters, default alphabet, CSPRNG |
| Message or event bus | UUIDv4 or UUIDv7 | v7 when consumers sort by time |
| Database primary key | UUIDv7 or ULID | Binary storage; uniqueness constraint mandatory |
| Log and trace correlation | ULID | Accept timestamp disclosure in the value |
| Human read-back codes | Crockford base32 or base58 | Short lengths need a volume-based collision review |
| Secrets and reset tokens | Dedicated random token | Hash at rest, expire, single use |
| Internal-only surrogate key | 64-bit integer | Never expose; pair with a public identifier if needed |
Use the NanoID Generator to inspect length and alphabet choices, then confirm those settings in your runtime; the UUID Generator does the same for standard versions.
11. Common mistakes
- Assuming unique means authorized: every read still needs access control.
- Using timestamps alone: timestamps are predictable and collide under concurrency.
- Changing the alphabet after launch: existing IDs and validators may break.
- Skipping a uniqueness constraint: application checks race under concurrent writes.
- Logging secrets: reset links and session tokens should not appear in ordinary logs.
- Sorting without a contract: lexical order only matches chronology for compatible formats.
- Truncating identifiers for display without a plan: a six-character prefix of a ULID keeps almost none of the collision resistance; if you shorten for humans, recompute the birthday bound for the truncated length.
- Assuming cross-process ULID order: monotonicity holds inside one generator; between writers with skewed clocks, order is only approximate.
- Regexes that only match UUIDv4: a pattern keyed to the version-4 nibble silently rejects v7 values the day you adopt them; match the general UUID shape and check the version separately.
- Case-folding the key column: a case-insensitive collation on NanoID or base32 keys merges distinct values into one row.
Conclusion
UUID is the compatibility baseline, NanoID is a compact public-ID option, and ULID is useful when time ordering belongs in the design; UUIDv7 is the same ordering idea wearing hex. Judge each candidate by entropy per character, what its bits leak, how its shape behaves inside a B-tree, and whether generation must survive twelve writers with no shared clock. Define those four properties before choosing, keep a uniqueness constraint regardless, and the identifier will still make sense when the next engineer inherits the schema.
Frequently Asked Questions
- Which is shortest?
- NanoID is commonly shorter than a textual UUID, but length should not be the only criterion.
- Is NanoID more secure than UUID?
- Neither is automatically secure; security depends on the version, random source, length, and protection.
- Are ULIDs sortable?
- Their timestamp prefix makes lexical ordering approximately chronological when generated and compared consistently.
- Can I use an ID as a password reset token?
- Use a dedicated high-entropy, expiring, single-use token and store a hash.
- Do I need a uniqueness constraint?
- Yes. Probability is not a substitute for a database constraint.
- Does ULID reveal information?
- It can reveal creation time because the timestamp is encoded in the value.
- What is UUIDv7?
- UUIDv7 places a millisecond timestamp in the high bits followed by random bits, so values sort lexically by creation time while keeping most of the entropy of a random identifier.
- Is the default NanoID alphabet URL-safe?
- Yes. The default alphabet uses letters, digits, underscore, and hyphen, so a value can appear in a path segment or query string without percent-encoding.