What a NanoID is
A NanoID is the simplest identifier design there is: pick an alphabet, pick a length, and draw that many characters uniformly at random from a cryptographic source. There is no timestamp, no counter, no machine identifier, no version field and no internal structure of any kind. The whole value is entropy.
The default configuration is 21 characters from a 64-symbol alphabet — the uppercase letters, the
lowercase letters, the digits, plus _ and -. Sixty-four symbols is exactly six
bits each, so a default NanoID carries 126 bits of entropy. That number is not a
coincidence: a UUID v4 carries 122 random bits in 36 characters, so 21 characters buys you slightly more
unpredictability in 15 fewer characters. The choice of _ and - as the two
non-alphanumeric symbols is the same choice base64url makes, and for the same reason — both survive a
URL path, a query string, a filename and a DOM id without escaping.
That is the entire appeal. Where a UUID is a standard with a binary layout, a version registry and decades of database support, a NanoID is a convention you can reimplement in ten lines. The trade is deliberate: you give up the ecosystem and gain the ability to say "seven characters, uppercase and digits only, because this goes on a printed voucher".
Why modulo is wrong, and what this page does instead
This is the part that separates a correct generator from one that merely looks correct, and it is worth following carefully because the broken version produces output that passes every casual inspection.
Suppose you want characters from a 62-symbol alphabet and you have a source of random bytes. The obvious
approach is alphabet[byte % 62]. A byte holds 256 equally likely values. Dividing 256 by 62
gives 4 with a remainder of 8. So values 0–247 distribute evenly — four bytes map to each of the 62
symbols — but the leftover values 248–255 map to symbols 0 through 7 a fifth time. Those eight symbols
therefore appear with probability 5/256 while the other fifty-four appear with probability 4/256.
The first eight characters of your alphabet are 25% more likely than the rest.
Nothing about the output looks wrong. Every identifier is still unpredictable in casual terms; you cannot spot the skew by reading a few of them. But the entropy is genuinely lower than the arithmetic suggests, the distribution is a fingerprint of the implementation, and in the pathological case — an alphabet size just over a power of two, say 129 symbols — the bias becomes enormous, with the first symbols appearing nearly twice as often as the last.
The fix is rejection sampling, sometimes called mask-and-reject. Instead of folding out-of-range values back into the alphabet, discard them and draw again:
- Compute the smallest bit mask that covers the alphabet size. For 62 symbols that is
0b111111= 63; for 10 symbols it is0b1111= 15. - Take a random byte and apply the mask. The result is uniform over 0 … mask.
- If the result is less than the alphabet size, use it. Otherwise throw it away and take the next byte.
Every accepted value is now equally likely, because every accepted value came from the same uniform range and no value was mapped to twice. The cost is that some bytes are wasted — for a 10-symbol alphabet, six of every sixteen masked values are rejected, so you draw about 1.6 bytes per character. That cost is invisible at any realistic scale, and it is the only way to be uniform when the alphabet size is not a power of two.
Two implementation details this page gets right and naive versions often do not. The random pool is
requested in one crypto.getRandomValues call sized generously for the expected rejection
rate, rather than one call per character, because per-character calls are dramatically slower. And when
the pool is exhausted mid-identifier, a fresh pool is drawn and generation continues — the loop is
driven by "have I got enough characters yet", never by "have I used all my bytes", which is the bug that
produces short identifiers under an unlucky draw.
One happy special case: when the alphabet size is a power of two, the mask covers exactly the alphabet
and nothing is ever rejected. The 64-symbol default is such a case, which is why byte & 63
is safe there — and why testing a generator only against the default alphabet will never reveal a modulo
bug. Switch this page to the digits-only preset if you want to exercise the rejection path.
Reading the collision calculator
Collisions in a random identifier space follow the birthday problem, not intuition. The relevant number is not "how many possible values are there" but "how many values before two of them coincide", and the second is roughly the square root of the first. A space of 1018 values does not survive 1018 draws — it starts colliding around 109.
The calculator uses the standard approximation for the number of draws n giving collision probability p over a space of N values: n ≈ √(2N·ln(1/(1−p))). It reports the count for a 1% chance and for a 50% chance, then converts both into elapsed time at the generation rate you enter. All of it is computed in logarithms, because N for a default NanoID is about 1037 and would otherwise overflow a double long before the square root.
What the output tends to teach:
- The default is absurdly safe. 126 bits at a million identifiers per second stays below a 1% collision chance for longer than the universe has existed. Anyone worrying about NanoID collisions at the default settings is worrying about the wrong thing.
- Short identifiers fail much sooner than people expect. Eight alphanumeric characters is about 47.6 bits — roughly 1014 possibilities — and the first collision becomes likely in the low millions. For a URL shortener doing a few thousand links a day that is fine for years; for an event ID at a thousand per second it is a matter of days.
- Length beats alphabet size. Each extra character multiplies the space by the alphabet size; switching from 62 symbols to 64 multiplies it by 1.03 per character. If you need more headroom, add a character rather than hunting for exotic symbols.
- A uniqueness constraint changes the risk, not the maths. A unique index turns a silent collision into a failed insert you can retry. That is the difference between a data-corruption incident and a 409 Conflict, and it costs one line of DDL.
The last point deserves emphasis, because it is the one that actually matters in production. Real
duplicate identifiers almost never come from exhausting the entropy space. They come from a broken
random source: a library falling back to Math.random, a container fleet whose instances all
seed identically at boot, an embedded device with no entropy, or code that generates one identifier and
caches it across requests. Put a unique constraint on the column and you will find out immediately.
Trust the arithmetic and you will find out from a customer.
Choosing an alphabet
The alphabet is where NanoID earns its keep over a fixed-format identifier, and the right choice comes from asking who or what will handle the value.
| Situation | Alphabet | Why |
|---|---|---|
| URLs, filenames, DOM ids | URL-safe 64 | No escaping anywhere, maximum entropy per character |
| Case-insensitive systems | Lowercase + digits (36) | Hostnames, some file systems and several databases fold case; a mixed-case ID collapses and collides |
| Read aloud or typed by hand | No look-alikes | Removing 0/O, 1/I/l and similar pairs eliminates the whole class of transcription errors |
| Printed on paper or a screen photo | Uppercase + digits, short, hyphen-grouped | Legible at low resolution; grouping helps people keep their place |
| Interop with hex tooling | Hex (16) | Parses as a number, matches existing regexes, at the cost of 2 bits per character |
| Numeric-only fields | Digits (10) | Fits legacy systems that store identifiers as numbers — but 3.3 bits per character means you need length |
Two rules to keep in mind. First, entropy per character is log₂(alphabet size): 6 bits for 64 symbols, ~5.95 for 62, ~5.17 for 36, 4 for hex and ~3.32 for digits. The readout above shows the total for your current settings, and shrinking the alphabet always means lengthening the identifier to stay in the same place. Second, never start an identifier that will be used as a DOM id, a CSS selector or a hostname label with a digit or a hyphen — CSS identifiers cannot begin with a digit, and DNS labels cannot begin or end with a hyphen. If that applies, generate a first character from a letters-only alphabet and the rest from your full set.
NanoID against the alternatives
NanoID competes with UUID v4 for public identifiers and loses to time-ordered schemes for database keys. Being clear about which job you are doing settles the choice quickly.
Against UUID v4: both are pure randomness from a cryptographic source, and at default settings NanoID has marginally more of it. UUID wins on ecosystem — a native 16-byte column type in most databases, a universally recognised text format, validation built into every framework. NanoID wins on length and on flexibility. If the identifier lives in a database column, the 16-byte storage of a UUID is a real argument; if it lives in a URL a user might type, 21 characters against 36 is a real argument the other way. The UUID generator covers the UUID side of that comparison in full, including how random keys behave in a B-tree index.
Against ULID and UUID v7: not really a comparison, because they answer a different question. Those are time-ordered, so they sort by creation and keep index inserts local — and they publish their creation time to anyone holding one. NanoID sorts arbitrarily and reveals nothing. That makes NanoID a bad clustered primary key and a good public identifier, which is exactly the split most schemas end up with: a ULID or UUID v7 internally, a NanoID in the URL.
Against an auto-increment integer: NanoID exists partly to fix the enumeration problem.
Sequential IDs let anyone walk /orders/1 through /orders/n, count your
customers from a signup ID, and infer your growth rate from two data points. Unguessable identifiers
remove the easy path — although an endpoint that returns other people's data to an authenticated user
who guesses an ID is broken regardless, and should answer
403 Forbidden rather than relying on the ID being hard to find.
Against a random hex string: a NanoID is the same idea with a denser alphabet. Thirty-two hex characters and 21 NanoID characters carry almost the same entropy — 128 bits against 126 — so the choice is purely about which text form suits the context. Hex is friendlier to grep and to legacy parsers; NanoID is shorter.
Using generated identifiers in API work
- Public resource identifiers.
/documents/V1StGXR8_Z5jdHi6B-myTtells the world nothing about how many documents exist or when this one was made. - Share links and invite codes. Length is user-visible here, so the shorter form genuinely matters — but check the collision calculator before shortening below about 12 characters, and remember an unguessable link is still not an access control.
- Idempotency keys. A fresh identifier per attempt in an
Idempotency-Keyheader lets a server recognise a retry and return the original result. See the HTTP headers guide for where it belongs and the HTTP methods guide for which verbs need the help. - Test fixtures. Unique values per run stop tests colliding in a shared environment. Generate a batch here and paste them into a request body on the API tester.
- Correlation IDs. One value per inbound request, logged everywhere it goes, turns a distributed failure into a single greppable string. If you also want the time, use a ULID instead.
- Token identifiers. A NanoID works as a JWT
jticlaim where a denylist keys on it — the JWT generator will build the token around it.
And the standing caveat, which applies to every unguessable identifier: an identifier is not a
credential. Values in URLs end up in server logs, proxy logs, browser history, bookmarks and
Referer headers, and a "secret" link leaks the moment someone pastes it into a chat that
unfurls previews. Authenticate the request, then use the identifier to find the row.
Frequently asked questions
What is a NanoID?
A short random string drawn from a URL-safe alphabet. The default is 21 characters from 64 symbols — letters, digits, underscore and hyphen — giving 126 bits of entropy with no timestamp, counter or internal structure.
Why is 21 characters the default?
Twenty-one characters at six bits each is 126 bits, slightly more than a UUID v4's 122, in fifteen fewer characters. It is a parity point with UUID rather than a round number.
Why does a naive modulo introduce bias?
A byte has 256 values. With a 62-symbol alphabet, 256 ÷ 62 leaves a remainder of 8, so the first eight symbols get one extra byte mapped to them and appear 25% more often. This generator masks and rejects out-of-range draws instead, so every symbol is equally likely.
Is a NanoID as safe as a UUID?
At the defaults it has marginally more entropy — 126 bits against 122 — and both come from
crypto.getRandomValues here. The real difference is structural: UUID has version bits and a
standard 16-byte binary form, NanoID has neither.
Can I use a custom alphabet?
Yes, any set of two or more distinct characters. Shrinking the alphabet lowers entropy per character, so lengthen the identifier to compensate — the readout above shows the total as you change it.
Are NanoIDs sortable by creation time?
No. They contain no timestamp, so sorting gives an arbitrary order. That is a weakness for a clustered primary key and a strength for a public identifier.
How many can I generate before a collision?
At the defaults, around 1019 for a one percent chance — longer than the universe has existed at a million per second. Short identifiers are a different story, which is what the calculator is for.
Should a NanoID be my primary key?
Usually not the clustered one. Random keys scatter B-tree inserts and NanoID has no compact binary form. A time-ordered internal key with a NanoID as the unique public column gives you both properties.
Is a NanoID a secret?
Unguessable, but not confidential. URLs are logged, bookmarked and forwarded. Authenticate the request; do not let an identifier be the only barrier.
Related tools and reference
UUID Generator
The standardised 128-bit alternative, with a native database column type on most engines.
ULID Generator
When you need the identifier to sort by creation time instead of revealing nothing.
Random String Generator
The same randomness with free-form character-set toggles, for API keys and test fixtures.
Hash Generator
SHA-256 and friends, for when you need to store a digest of an identifier rather than the value.
API Tester
Send a generated identifier to an endpoint and see how it is handled.
409 Conflict
What a unique constraint should turn a collision into.