UUID vs NanoID vs ULID: Pick an ID Format on Purpose
Random IDs aren’t interchangeable. Sortability, length, and collision assumptions differ. Here’s a practical comparison for app builders.
Part of the Technical Foundations series.
Someone asks “what should we use for IDs?” and the room splits into camps. The useful answer depends on whether you need time-sortable keys, short public URLs, database locality, or boring interoperability with every library on earth.
Quick profiles
UUID (v4) - 122 bits of randomness in a familiar 8-4-4-4-12 hex shape. Universally supported. Not sortable by creation time. Verbose in URLs. Generate samples with UUID Generator; validate with UUID Validator. Prefer v7 when you want time-ordered UUIDs - try UUID v7 Generator.
NanoID - compact, URL-friendly alphabet, configurable length. Great for public tokens and short links. You’re responsible for choosing enough entropy for your threat model. Play with sizes in NanoID Generator.
ULID - 48-bit timestamp + randomness, Crockford base32, lexicographically sortable by time. Nice for log-friendly ids and DB indexes that like sequential inserts. Try ULID Generator.
Decision questions
- Will humans paste this into a browser bar? Prefer NanoID or ULID over raw UUID v4.
- Do you need creation-time ordering without a separate
created_at? ULID or UUID v7. - Must every enterprise vendor accept the format? UUID still wins politics.
- Are these secret capabilities (invite tokens)? Length and unpredictability matter more than aesthetics - don’t shrink NanoID into the danger zone.
Database notes
Random UUIDs as primary keys can fragment B-tree indexes. Time-ordered ids behave better for insert locality. That said, many teams run UUID v4 happily at moderate scale. Measure before you rewrite the universe.
Security notes
IDs in URLs leak to logs and Referers. Don’t treat a guessable sequence number as an authz check - ever. Opaque ids help; authorization still has to be explicit. For password-adjacent secrets, use a proper generator like Password Generator, not a short NanoID.
Public versus internal ids
Some teams use internal numeric ids and external opaque ids. That’s fine - just don’t leak the numeric ones in URLs if they’re guessable and authorization is sloppy. External NanoIDs paired with strong authz checks work well for share links.
If you ever expose sequential invoice numbers, that’s a business requirement - compensate with auth and rate limits, don’t pretend the number is secret.
Migration sketch if you chose poorly
- Add a new column for the new id format.
- Backfill in batches.
- Dual-read: accept either id on the API for a while.
- Switch writes to the new format.
- Stop accepting old ids after a sunset date.
Communicate early. Mobile clients and integrations linger longer than web deploys.
Libraries and interop
UUID wins when a partner system demands RFC shapes. ULID and NanoID shine inside your boundary. Document the choice in ADRs so the next debate doesn’t restart from zero six months later.
Field notes from teams who shipped this
The pattern that keeps showing up: write the constraint first, then the steps, then the failure modes. Teams that only publish happy-path screenshots create tickets. Teams that document the ugly path create trust.
Schedule a short review ninety days after publishing. Check whether product UI names still match, whether linked tools still exist, and whether support still hears the same questions. Update the page or merge it. Standing still is how useful posts become interchangeable again.
If you adapt this article for internal wikis, keep the examples tied to your stack names. The moment you generalize back to “best practices for organizations,” you’ve started erasing the specificity that made the piece worth saving.
Testing and fixtures
Deterministic tests hate pure randomness. Use seeded generators in unit tests or inject an id factory. Snapshot tests should not churn every run because a UUID changed.
In staging data, mark synthetic ids clearly when possible so nobody mistakes them for production references in screenshots shared externally.
Document canonical string formats in your API guidelines: uppercase vs lowercase hex for UUIDs, expected length for NanoIDs, and whether ULIDs are always Crockford base32. Clients get picky, and so do database collations.
When debugging production, never paste full id lists containing customer references into public channels - treat ids as semi-sensitive when they appear beside personal data.
Analytics and id formats
Analytics pipelines sometimes assume UUID shapes. If you pick NanoID, tell data eng before dashboards break. Same for warehouse primary keys and CDC tools. An id format is a cross-team contract, not a local preference.
Record the decision in an ADR with examples of valid and invalid strings for tests.
URLs, support, and human factors
Support agents read ids aloud over the phone. NanoID’s alphabet avoids ambiguous characters by design in many configs - confirm yours does. UUIDs are longer to dictate; ULIDs sit in between.
If agents often mis-hear ids, consider a short check digit or a support-only lookup code separate from the primary key. Don’t weaken entropy of security-sensitive tokens to make call centers happier - split the concerns.
Log search should allow case-insensitive lookup if you store mixed-case forms. Decide canonical casing once and normalize on write.
FAQ
Can we migrate later?
Yes, with dual-read/dual-write pain. Cheaper to pick deliberately now for new tables.
Will NanoID collide?
At sensible lengths and honest randomness, collisions are negligible for most apps. Don’t use 8 characters for a global object space and then act surprised.