UUID v4 is the default most people reach for, and it's a fine default: 122 bits of randomness, a format every database and language understands natively, and collision odds low enough to not think about — you'd need to generate over a billion UUIDs a second for roughly a hundred years before a collision became likely. The cost is length: 36 characters, mostly hyphens and hex, which is annoying in a URL, a log line, or a support ticket someone has to read aloud over the phone.
Nanoid trades some of that for compactness: a shorter, URL-safe string with a configurable alphabet and length, at the cost of not being a format databases treat specially. A default 21-character nanoid has about as much entropy as a UUID v4 in barely more than half the characters, because it isn't spending bits on the fixed hyphens and version markers a UUID reserves for itself. That's the real tradeoff — not security, but whether the ID needs to be a first-class type in your schema or just an opaque token nobody but your app ever parses.
There's a performance angle that's easy to miss until it bites you: random UUID v4 values make bad primary keys in a B-tree-indexed table at scale, because every insert lands at a random point in the index instead of appending to the end. That causes page splits and index fragmentation that a sequential or time-sortable ID doesn't. If you're indexing heavily on the ID and inserting at high volume, this is the actual reason teams reach for ULID or a UUID v7 instead of v4 — not for the string format, but for the sortable, mostly-sequential bit layout that keeps inserts index-friendly.
Nanoid has a smaller but real gotcha of its own: its default alphabet is case-sensitive, so `AbCdEf` and `abcdef` are different IDs. That's fine in a URL or an API response, but it's a problem the moment someone reads an ID aloud, types it into a support form by hand, or a case-insensitive system (some search indexes, some spreadsheet tools) normalizes it without telling you. If humans will ever transcribe the ID, a shorter alphabet without ambiguous characters (no `0`/`O`, no `1`/`l`) is worth the extra length.
Storage size is a minor consideration but not a zero one: a UUID stored in a proper `uuid` column is 16 bytes on disk regardless of its 36-character text representation, while a nanoid stored as text costs roughly one byte per character. At small scale this is noise; at hundreds of millions of rows with the ID appearing in several foreign-key columns, it adds up enough to show up in a storage bill.
Neither format is a substitute for a cryptographically secure random generator when the ID is actually a security token — a password-reset link, an API key, a session identifier. Both nanoid and UUID v4 libraries typically use a CSPRNG under the hood, but always confirm that before using either as anything more sensitive than an identifier, especially if a codebase has a faster, non-cryptographic random fallback wired in for performance reasons.
It's worth knowing the other UUID versions exist, even if v4 stays the default. UUID v1 encodes a timestamp and the generating machine's MAC address, which makes it sortable but leaks information you usually don't want to expose in a public-facing ID. UUID v5 is deterministic — it hashes a namespace and a name into the same UUID every time, which is genuinely useful for generating a stable ID from an external identifier (an email address, a URL) without a lookup table, but useless as a source of randomness since the same input always produces the same output. UUID v7, newer and increasingly supported, is the closest thing to "UUID v4 but sortable": a Unix timestamp in the high bits and randomness in the low bits, which gets you both the standard 128-bit UUID format and the index-friendly insert pattern that v4 lacks.
Migrating an existing table from UUID v4 to a sortable format isn't something to do casually — every foreign key referencing that column, every index built on it, and every client-side assumption about ID format has to move together, and a mid-migration state with two ID formats in the same column is its own source of bugs. It's much cheaper to pick deliberately at the start of a table's life than to revisit it once the table has millions of rows and a dozen foreign keys pointing at it.
Sharding and multi-region writes add one more wrinkle worth knowing about: fully random IDs (UUID v4, nanoid) are actually an advantage in a multi-writer setup, because two regions generating IDs simultaneously essentially never collide without needing to coordinate — no shared counter, no central sequence to synchronize. A naively sequential ID scheme, by contrast, needs real coordination across writers to avoid collisions, which is exactly the coordination cost horizontally-scaled systems are usually trying to avoid. This is part of why time-sortable formats like ULID and UUID v7 still keep a large random component instead of just being a plain counter — they get most of the sort-order benefit while keeping the collision-free property that made random IDs attractive in a distributed system to begin with.
A practical rule: reach for UUID v4 for anything that's a primary key or gets stored in a UUID-typed column and doesn't need to be human-typed, reach for ULID or UUID v7 when you specifically need time-sortable IDs at insert-heavy scale, and reach for a shorter nanoid-style ID for anything user-facing — short links, invite codes, session tokens shown in a URL. Optimize for what a human, a database index, or a URL bar actually has to deal with, not for entropy you don't need.
More from the blog
5 Habits for Debugging Messy JSON Payloads Faster
The difference between a two-minute payload review and a twenty-minute one usually comes down to a few small habits.
SQL Formatting Conventions That Make Code Review Easier
A query that's easy to review is one where the diff shows the actual change, not a reflow of every line around it.
Testing Webhooks Locally Without Exposing Your Machine
You don't need to punch a hole in your firewall to see what a provider actually sends before your endpoint touches it.