NanoID: Why Some Developers Are Ditching UUIDs for Shorter IDs
A UUIDv4 spends 36 characters — 32 hex digits and 4 hyphens — to encode 122 bits of randomness. NanoID asks a simple question: what if you used a bigger alphabet instead of hexadecimal? The answer is a 21-character ID with more entropy than a UUIDv4, no hyphens, no fixed structure, and full control over both the character set and the length. This post digs into how NanoID achieves that, where it does and doesn't make sense as a UUID replacement, and how to reason about the collision math when you go tune the defaults.
The Core Insight: Alphabet Size Determines Bits per Character
A UUID's text form is built from hexadecimal digits — 16 possible symbols per position, or exactly 4 bits per character (2^4 = 16). That's why a 128-bit UUID needs 32 hex characters (plus 4 structural hyphens) to represent itself as text.
NanoID's default alphabet is A-Za-z0-9_- — 64 symbols, giving 6 bits per character (2^6 = 64) instead of 4. At NanoID's default length of 21 characters, that's:
21 characters × 6 bits/character = 126 bits of entropy
Compare that to UUIDv4's 122 random bits (128 total, minus 6 fixed version/variant bits). NanoID's default configuration actually has more entropy than UUIDv4, while using 15 fewer characters — a ~42% reduction in string length for a comparable (in fact very slightly stronger) collision guarantee.
| Format | Alphabet Size | Bits per Character | Characters | Total Entropy |
|---|---|---|---|---|
| UUIDv4 (hex + hyphens) | 16 | 4 | 32 (+4 hyphens) | 122 bits |
| NanoID (default) | 64 | 6 | 21 | 126 bits |
| NanoID (custom, base62) | 62 | ~5.95 | 21 | ~125 bits |
| Short NanoID | 64 | 6 | 10 | 60 bits |
The Collision Math
Both UUIDv4 and NanoID rely on the same underlying principle — the birthday paradox — but it's worth working through explicitly, since it's what lets you safely tune NanoID's length down for a given use case.
For an ID space of size N (total possible values), the probability of at least one collision after generating k IDs is approximately:
P(collision) ≈ k² / (2N)
For NanoID's default settings (N = 64^21 ≈ 1.21 × 10^38), reaching a 1% collision probability requires generating approximately:
k ≈ √(2N × ln(1 / (1 - 0.01))) ≈ 1.56 × 10^18 IDs
That's on the order of a quintillion IDs before collision risk becomes meaningfully non-zero — comfortably beyond what any single application will generate. This is what the NanoID README means when it says the default is sized "to have a collision probability similar to UUID v4."
// Approximating collision probability for a given alphabet/length/count
function collisionProbability(alphabetSize, idLength, count) {
const N = Math.pow(alphabetSize, idLength);
return (count * count) / (2 * N);
}
// Default NanoID at 1 million generated IDs
collisionProbability(64, 21, 1_000_000); // ≈ 4.1e-27 — effectively zero
The practical implication: you can shrink the ID length considerably for lower-cardinality use cases (short-lived tokens, low-volume tables) and still be safely inside acceptable collision odds — as long as you actually run the numbers for your expected volume rather than guessing.
Code Examples
JavaScript — default and custom configurations
import { nanoid, customAlphabet } from 'nanoid';
// Default: 21 characters, 64-symbol alphabet, ~126 bits entropy
const id = nanoid(); // "V1StGXR8_Z5jdHi6B-myT"
// Shorter ID for a lower-volume, less collision-sensitive use case
const shortId = nanoid(10); // "IRFa-VaY2b"
// Custom alphabet — e.g. avoid visually ambiguous characters for
// IDs a human might need to read aloud or type manually
const readableId = customAlphabet('23456789ABCDEFGHJKLMNPQRSTUVWXYZ', 8);
const code = readableId(); // "7GQXP2FH" — no 0/O, 1/I/l confusion
C# — using a NanoID port
// Nanoid.net (or similar ports) mirror the JS API
using NanoidDotNet;
string id = Nanoid.Generate(); // default: 21 chars, standard alphabet
string shortId = Nanoid.Generate(size: 10); // custom length
string customId = Nanoid.Generate(
alphabet: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", // custom alphabet
size: 12
);
SQL — storage considerations
Unlike UUIDs, there's no native database type for NanoID — it's just a string, so store it accordingly and size the column to your chosen length exactly (don't over-allocate):
CREATE TABLE ShortLinks (
Id CHAR(21) PRIMARY KEY, -- fixed-length, matches default NanoID size
TargetUrl NVARCHAR(2048) NOT NULL,
CreatedAt DATETIME2 NOT NULL
);
-- Postgres equivalent
CREATE TABLE short_links (
id CHAR(21) PRIMARY KEY,
target_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Because there's no native binary encoding the way UNIQUEIDENTIFIER/UUID types provide, a NanoID column is always stored as its full character length — there's no equivalent of collapsing it to 16 raw bytes the way you can with a UUID. This is a genuine trade-off worth weighing against the string-length savings: NanoID wins on readability and URL length, not necessarily on raw storage bytes, unless you're comparing against UUIDs stored as their bloated 36-character text form rather than as native 16-byte values.
Why Developers Reach for It
URLs and user-facing identifiers. A UUID in a URL (/s/550e8400-e29b-41d4-a716-446655440000) is long, has hyphens that sometimes get mangled by copy-paste or messaging apps, and looks intimidating. A NanoID (/s/V1StGXR8_Z5jdHi6B-myT) is shorter, hyphen-free by default, and URL-safe out of the box — this is precisely the use case (Evil Martians, NanoID's creators, built it) it was designed for: short-link services, invite codes, and public-facing resource IDs.
Configurable alphabets for specific constraints. Need an ID safe to read aloud over the phone, or safe for a case-insensitive filesystem, or restricted to characters that won't be flagged as profanity when concatenated? NanoID's customAlphabet makes that a one-line change; achieving the same with UUIDs would mean re-encoding the raw bytes through your own alphabet entirely.
Performance. Independent of the entropy math, NanoID's reference JavaScript implementation is benchmarked as faster than both crypto.randomUUID() and the uuid package's v4 implementation, because it pulls exactly the number of random bytes it needs for the chosen alphabet and length rather than following the fixed UUID byte-layout logic.
Where NanoID Falls Short of UUIDs
No standard. UUIDs are defined by RFC 9562 and universally recognized by databases, ORMs, log parsers, and validation libraries. A NanoID is just a string in the shape you configured — nothing enforces or documents its format outside your own codebase, so a teammate (or another service) has no external spec to check against.
No time-ordering option (without doing it yourself). UUIDv7 gives you time-sortable, database-friendly clustering out of the box. NanoID's output is purely random — there's no equivalent to a "NanoID v7." If you need both short IDs and time-ordering, you'd need to prepend your own timestamp component and manage that encoding yourself.
No native database type. As shown above, UUIDs get first-class UNIQUEIDENTIFIER/UUID types with binary storage and native comparison in most major databases. NanoID is always a plain string column, which means index comparisons happen on the string rather than a fixed-width binary value.
Fragmented ecosystem. NanoID has ports in 20+ languages, but they're independent implementations of the algorithm, not a single governed standard — subtle differences in default alphabet ordering or randomness source can exist between ports, which matters if you're generating IDs from multiple languages and expect bit-for-bit identical guarantees.
NanoID vs. UUID vs. ULID: Comparison Table
| Property | UUIDv4 | UUIDv7 | NanoID (default) | ULID |
|---|---|---|---|---|
| Standardized (RFC/spec) | ✅ RFC 9562 | ✅ RFC 9562 | ❌ (community convention) | ❌ (community spec) |
| Character length | 36 | 36 | 21 (configurable) | 26 |
| Entropy | 122 bits | ~74 random bits + timestamp | 126 bits (configurable) | 80 random bits + timestamp |
| Time-sortable | ❌ | ✅ | ❌ | ✅ |
| Native DB type | ✅ (most engines) | ✅ (most engines) | ❌ — plain string | ❌ — plain string |
| URL-safe by default | ❌ (hyphens) | ❌ (hyphens) | ✅ | ✅ |
| Configurable alphabet/length | ❌ | ❌ | ✅ | ❌ |
Choosing Between Them
- Reach for UUIDv4 when you need a widely recognized, standardized format with native database support and don't care about string length.
- Reach for UUIDv7 when you need that same standardization and want time-ordered, index-friendly clustering as a primary key.
- Reach for NanoID when the ID is user-facing (URLs, short codes, invite links), you want control over length and character set, and you don't need a formal external standard or a native database type.
- Reach for ULID when you want NanoID-like brevity and Base32 readability combined with time-ordering, and are willing to accept a less mature ecosystem than UUID's.
None of these is a strict upgrade over the others — they optimize for different constraints. NanoID's real contribution isn't "smaller UUID," it's decoupling ID length and alphabet from a fixed spec, so you can tune both to the actual entropy your use case requires instead of always paying UUID's 36-character tax regardless of whether you need that much collision resistance.