Nano ID generator
Nano IDs from the 64-symbol URL-safe alphabet, default length 21, generated locally from the operating system CSPRNG.
What a Nano ID is
A Nano ID is a string drawn uniformly from a fixed 64-symbol alphabet. The default length is 21, set in the reference implementation as customAlphabet(alphabet, defaultSize = 21). The symbol set is A-Za-z0-9_-, which is the RFC 4648 section 5 base64url alphabet with plus and slash replaced. Every one of those 64 characters is unreserved in RFC 3986, so a Nano ID needs no percent-encoding in a path segment, a query value or a fragment. It is also safe in a POSIX or Windows filename and safe unquoted in a shell.
Sixty-four symbols is six bits per character with nothing left over, so a 21-character Nano ID carries exactly 126 bits. A version 4 UUID carries 122 bits in 36 characters, because 128 bits minus four version bits minus two variant bits is 122, and the hyphens carry nothing at all. Nano ID is shorter and slightly wider. Its README makes the comparison itself: "It has a similar number of random bits in the ID (126 in Nano ID and 122 in UUID)".
Two characters in that alphabet cause trouble at the edges. A Nano ID can begin with an underscore, which CouchDB and PouchDB reject in a document ID, and the README recommends prefixing the value for exactly that reason. It can also begin with a hyphen, which an argument parser will read as a flag and which is not valid at the start of an HTML id or a CSS selector without escaping. If either of those is your environment, force a leading letter or use a different format.
| Length | Bits | IDs for a 1% collision chance | At 1,000 per second |
|---|---|---|---|
| 8 | 48 | 2.38e6 | About 40 minutes |
| 10 | 60 | 1.52e8 | About 1.8 days |
| 12 | 72 | 9.74e9 | About 0.31 years |
| 16 | 96 | 3.99e13 | About 1,265 years |
| 21 (default) | 126 | 1.31e18 | About 4.1e7 years |
| 24 | 144 | 6.70e20 | About 2.1e10 years |
Why masking with 63 is exact and masking with 62 is not
The browser build of nanoid takes one random byte per character and writes urlAlphabet[byte & 63]. One getRandomValues call, no loop, no discarded draws. That is unusual for a random-string generator, and it is correct, because 64 divides 256 exactly: each of the 64 indices has exactly four of the 256 byte values landing on it, so every character is equally likely.
Change the alphabet to 62 symbols and the same one-liner becomes the textbook modulo-bias defect. 256 is 4 times 62 plus 8, so residues 0 through 7 have five source bytes each and residues 8 through 61 have four. The ratio is exactly 1.25, which means the first eight symbols come out 25 percent more often than the rest. We measured it over 20,054,016 draws from getRandomValues: symbol 0 at frequency 0.01957 against an expected 5/256 = 0.01953, symbol 30 at 0.01560 against 4/256 = 0.01563, observed ratio 1.2551, chi-square 132,618.6 on 61 degrees of freedom against a critical value of 100.888 at p = 0.999.
The cost is easy to understate, because the usual measure hides it. Shannon entropy falls by only 0.0045 bits per character. Min-entropy, which is the figure that governs an attacker guessing most-likely-first, falls by 0.276 bits per character. Over 16 characters that is 4.418 bits, so the search space an attacker actually faces is roughly 21 times smaller than the tool claims.
nanoid does use rejection sampling, but only where it has to. Its customRandom path computes safeByteCutoff = 256 - (256 % alphabet.length) and throws away any byte at or above that cutoff before taking the modulo. When the alphabet length is a power of two the cutoff comes out at 256, nothing is ever rejected, and the code switches to the mask. So "nanoid uses rejection sampling" and "nanoid just masks a byte" are both true, of different code paths, and most write-ups pick one and state it as the whole story.
The mask is only exact because of the alphabet size. Copy byte & 63 and then swap in a 62-character or 49-character alphabet and you have introduced the bias the mask happened to avoid.
The alphabet the library actually stores
Print the default alphabet from the nanoid package and this is what comes back: useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict. It is not sorted, and it is not a mistake. The source comment says the ordering is "optimized for better gzip and brotli compression": the scramble embeds substrings that match the Brotli static dictionary and earlier bytes of the same file, which makes the minified bundle smaller.
The set is exactly A-Za-z0-9_-. The order is not. Both statements are true at once, and the difference matters if you are implementing Nano ID yourself. Ordering has no effect on the output distribution, because every index is equally likely either way, so a from-scratch implementation using the sorted string is fully compatible. What is wrong is printing the sorted string and labelling it "the nanoid alphabet". That is the set, not the string the library holds.
One thing the alphabet is not is unambiguous. It contains 0 and O, and 1 and l and I. If a person is going to read your identifier off a screen or a printed card and type it back in, Nano ID is the wrong alphabet. Crockford Base32 or the nanoid-dictionary nolookalikes set exists for that job.
Collisions, and the 1 percent against 50 percent trap
The nanoid README links to Zelark's collision calculator, and its source gives the formula it uses: bits = length times log2(alphabetSize), then n(p) = sqrt(2 times 2^bits times ln(1 / (1 - p))). That is the square approximation to the birthday problem, and the calculator hard-codes p = 0.01.
So every number it reports is the count at which you reach a 1 percent chance of at least one collision, not the 50 percent bound that most birthday-problem discussion uses. The two are not interchangeable. At the same bit count they sit a factor of sqrt(ln 2 / ln(1 / 0.99)) = 8.3 apart. Quoting one figure and comparing it against the other is how a format ends up looking an order of magnitude safer or worse than it is, and it happens often enough in format comparisons to be worth checking every time.
At the default 21 characters and 126 bits, the 1 percent threshold is 1.31e18 identifiers. At a thousand per second that is roughly 4.1e7 years of continuous generation. Whichever convention you prefer, state it in the same sentence as the number.
One more detail from that calculator worth copying: it de-duplicates the alphabet before counting it. Paste an alphabet with a repeated character and the effective base silently drops rather than raising an error, which quietly overstates nothing and understates the reported figure.
Where Nano ID sits among the alternatives
Nano ID does one job. It produces a short, unguessable, URL-safe identifier that reveals nothing about when it was made or which machine made it, and it needs no coordination between generators. That is the right default for anything that appears in a public URL.
If you also need values that sort by creation time, that job belongs to UUIDv7, ULID or KSUID, and all three buy sorting by publishing the creation time inside the identifier. There is no sortable format that hides when it was made, so the choice is a trade, not an upgrade.
For a secret rather than an identifier, length is the lever. The default 21 characters give 126 bits, just under the 128-bit convention; 22 characters give 132 and clear it. Nothing in a Nano ID is derived from time, sequence or host, which is what makes it usable as a token in the first place.
| Format | Characters | Random bits | Sorts by time | In a URL | Reveals creation time |
|---|---|---|---|---|---|
| Nano ID (21) | 21 | 126 | No | Yes, no escaping. Can start with - or _. | No |
| UUID v4 | 36 | 122 | No | Yes, 32 hex digits and 4 hyphens | No |
| UUID v7 | 36 | 74 | Yes, to the millisecond | Yes, same 36 characters | Yes, to the millisecond |
| ULID | 26 | 80 | Yes, to the millisecond | Yes, alphanumeric, no hyphens | Yes, to the millisecond |
| KSUID | 27 | 128 | Yes, to the second | Yes, alphanumeric, no hyphens | Yes, to the second |
Doing this in your own code
JavaScript
import { nanoid, customAlphabet } from "nanoid";
nanoid(); // 21 characters, 126 bits
nanoid(10); // 10 characters, 60 bits
// A different alphabet. This path uses rejection sampling,
// because 49 does not divide 256.
const readable = customAlphabet("346789ABCDEFGHJKLMNPQRTUVWXYabcdefghijkmnpqrtwxyz", 12);
readable();
// Without the dependency. The mask is exact only because the
// alphabet is 64 symbols and 64 divides 256.
const URL_ALPHABET =
"useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
function nanoidLocal(size = 21) {
const bytes = crypto.getRandomValues(new Uint8Array(size));
let id = "";
for (let i = 0; i < size; i++) id += URL_ALPHABET[bytes[i] & 63];
return id;
} nanoid/non-secure exists and uses Math.random(). Import it only for cache keys and similar, never for anything that must be unguessable.
Python
# pip install nanoid
from nanoid import generate
generate() # 21 characters from the default alphabet
generate(size=10)
generate("0123456789abcdef", 32) # custom alphabet
# Without the dependency, using the same masking trick:
import secrets
ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"
def nanoid(size: int = 21) -> str:
return "".join(ALPHABET[b & 63] for b in secrets.token_bytes(size)) secrets.token_bytes draws from os.urandom. The random module does not and must not be used here.
Go
import gonanoid "github.com/matoous/go-nanoid/v2"
id, err := gonanoid.New() // 21 characters
id, err = gonanoid.New(10) // 10 characters
// Custom alphabet and size.
id, err = gonanoid.Generate("0123456789abcdef", 32) Rust
// nanoid = "0.5"
use nanoid::nanoid;
let id = nanoid!(); // 21 characters
let id = nanoid!(10); // 10 characters
// Custom alphabet, passed as a &[char].
let alphabet: [char; 16] = ['0','1','2','3','4','5','6','7',
'8','9','a','b','c','d','e','f'];
let id = nanoid!(32, &alphabet); PHP
<?php
// No dependency needed. random_bytes() is the CSPRNG.
const NANOID_ALPHABET =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
function nanoid(int $size = 21): string {
$bytes = random_bytes($size);
$id = '';
for ($i = 0; $i < $size; $i++) {
$id .= NANOID_ALPHABET[ord($bytes[$i]) & 63];
}
return $id;
} random_bytes() throws on entropy failure rather than returning weak output, which is the behaviour you want.
Shell
# 21 characters of base64url from the kernel CSPRNG.
# The symbol set is identical to nanoid's; only the order differs.
head -c 16 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=' | head -c 21; echo
# Note: 16 bytes through base64 is 128 bits, and the trailing
# character of a 16-byte base64 value is always one of A Q g w.
# Truncating to 21 characters here drops that constraint. Questions
Is a Nano ID as secure as a UUIDv4?
At the default length it carries slightly more randomness: 126 bits against 122, in 21 characters instead of 36. Both are drawn from the operating system CSPRNG here. The real difference is not security but shape, since a UUID has a standard textual form that databases and libraries recognise and a Nano ID does not.
Can I put a Nano ID straight into a URL?
Yes. All 64 symbols are unreserved characters in RFC 3986, so no percent-encoding is needed in a path segment, a query value or a fragment. The one thing to watch is a leading hyphen or underscore, which some systems treat specially: CouchDB and PouchDB reject a document ID beginning with an underscore, and a command-line parser will read a leading hyphen as a flag.
What length should I use?
Twenty-one is the library default and gives 126 bits, which is more than enough for identifiers at any scale you will meet. Use 22 if you want a secret at 132 bits. Go shorter only for values that are scoped to something else, such as a short link inside one account, and check the collision table above before you do.
Do Nano IDs sort by creation time?
No. Every character is random, so a list of them sorts arbitrarily. If you need time ordering, use UUIDv7 or ULID and accept that both disclose the creation time to anyone holding the value.
Does nanoid use rejection sampling?
Only for alphabets whose size does not divide 256. For the default 64-symbol alphabet it masks each byte with 63, which is already exactly uniform, so nothing is ever rejected. For a custom alphabet it computes a cutoff at the largest multiple of the alphabet length below 256 and discards bytes above it. Both behaviours live in the same library.
Does this page use the nanoid package?
No. It uses the same 64-symbol set and the same masking, over the same crypto.getRandomValues that the library calls. Loading a dependency to write one loop would add weight without adding correctness, and the code is in the page source if you want to read it.