Last Updated: 3/9/2026
How It Works
Understanding Nano ID’s internal architecture helps you use it effectively and debug issues.
High-Level Overview
Nano ID generates random IDs through three core steps:
- Generate random bytes using hardware random generator
- Map bytes to alphabet using bitwise operations
- Return ID string of requested length
import { nanoid } from 'nanoid'
nanoid() // "V1StGXR8_Z5jdHi6B-myT"
// ↑
// 21 characters from urlAlphabet (A-Za-z0-9_-)Random Pool Architecture
Nano ID uses a pooled random generation strategy to minimize expensive system calls.
The Problem
Calling the hardware random generator (like crypto.getRandomValues()) is expensive:
// Expensive: one system call per ID
for (let i = 0; i < 1000; i++) {
crypto.getRandomValues(new Uint8Array(21)) // 1000 system calls
}The Solution: Random Pool
Nano ID generates a large pool of random bytes upfront, then consumes them:
// Efficient: one system call for many IDs
const pool = crypto.getRandomValues(new Uint8Array(21 * 128)) // 1 system call
for (let i = 0; i < 1000; i++) {
// Consume from pool (no system calls)
}Implementation
const POOL_SIZE_MULTIPLIER = 128
let pool, poolOffset
function fillPool(bytes) {
if (!pool || pool.length < bytes) {
// Initial allocation: 128x requested size
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER)
crypto.getRandomValues(pool)
poolOffset = 0
} else if (poolOffset + bytes > pool.length) {
// Pool exhausted: refill
crypto.getRandomValues(pool)
poolOffset = 0
}
poolOffset += bytes
}Key benefits:
- 128 IDs generated per system call
- Automatic refill when pool exhausted
- Memory-efficient (reuses buffer)
Alphabet Mapping
Nano ID maps random bytes (0-255) to alphabet characters using a bitmask.
The Default Alphabet
const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
// ↑ 64 characters (2^6)The alphabet is carefully ordered for optimal compression:
- Gzip/Brotli references:
'use,andom,rict'appear in many files - Brotli dictionary:
26T,1983,40px,bush,jack,mind,very,wolf
Mapping Algorithm
Since the alphabet has 64 characters (2^6), we use a 6-bit mask:
export function nanoid(size = 21) {
fillPool(size)
let id = ''
for (let i = poolOffset - size; i < poolOffset; i++) {
// Mask byte to 0-63 range (6 bits)
id += urlAlphabet[pool[i] & 63]
// ↑
// Binary: 0b00111111
}
return id
}Why & 63 works:
Random byte: 11010110 (214 in decimal)
Mask (63): 00111111 (63 in decimal)
--------
Result: 00010110 (22 in decimal)
↑
Maps to urlAlphabet[22] = '8'This ensures:
- ✅ Uniform distribution: All characters have equal probability
- ✅ No waste: Every random byte becomes a character
- ✅ Fast: Bitwise AND is extremely fast
Custom Alphabet Algorithm
For custom alphabets, the algorithm is more complex because alphabet sizes aren’t always powers of 2.
The Challenge
With a 30-character alphabet:
const alphabet = '0123456789ABCDEFGHIJKLMNOPQR' // 30 charsWe can’t use a simple bitmask because 30 isn’t a power of 2.
The Solution: Rejection Sampling
export function customAlphabet(alphabet, size = 21) {
// Find smallest power of 2 that exceeds alphabet size
let mask = (2 << (31 - Math.clz32((alphabet.length - 1) | 1))) - 1
// For 30 chars: mask = 31 (0b00011111)
// Calculate how many random bytes to request
let step = Math.ceil((1.6 * mask * size) / alphabet.length)
return (customSize = size) => {
let id = ''
while (true) {
let bytes = random(step)
let i = step
while (i--) {
// Map byte to alphabet, reject if out of range
id += alphabet[bytes[i] & mask] || ''
// ↑
// Returns '' if index >= alphabet.length
if (id.length >= customSize) return id
}
}
}
}How it works:
-
Mask calculation: Find smallest power of 2 ≥ alphabet size
- 30 chars → mask = 31 (2^5 - 1)
-
Rejection sampling: Discard bytes that map beyond alphabet
- Byte 28 & 31 = 28 → alphabet[28] ✅
- Byte 30 & 31 = 30 → alphabet[30] = undefined → reject ❌
-
Redundancy factor: Request 1.6x bytes to minimize rejections
- Magic number
1.6optimized through benchmarking
- Magic number
Why Not Modulo?
A common mistake is using modulo:
// WRONG: Creates bias
id += alphabet[randomByte % alphabet.length]This creates non-uniform distribution:
Alphabet size: 30
Random bytes: 0-255
0-29: 9 occurrences each (0, 30, 60, 90, 120, 150, 180, 210, 240)
30-255: 8 occurrences each
Result: First 30 chars are 12.5% more likely!Rejection sampling ensures perfect uniformity.
Browser vs Node.js
Nano ID has two implementations optimized for each environment.
Node.js (index.js)
import { webcrypto as crypto } from 'node:crypto'
// Uses pooled random generation
function fillPool(bytes) {
pool = Buffer.allocUnsafe(bytes * 128)
crypto.getRandomValues(pool)
}Optimizations:
- Buffer pooling (128x multiplier)
- Reuses allocated memory
- Batch system calls
Browser (index.browser.js)
// Direct crypto API, no pooling
export let nanoid = (size = 21) => {
let id = ''
let bytes = crypto.getRandomValues(new Uint8Array(size))
while (size--) {
id += urlAlphabet[bytes[size] & 63]
}
return id
}Why no pooling?
- Browsers already optimize
crypto.getRandomValues() - Memory pooling less beneficial in browser context
- Simpler code = smaller bundle size
Module Resolution
Bundlers automatically select the right version:
// package.json
{
"browser": {
"./index.js": "./index.browser.js"
},
"react-native": {
"./index.js": "./index.browser.js"
}
}Performance Characteristics
Time Complexity
- nanoid(): O(n) where n = size
- customAlphabet(): O(n × r) where r = rejection rate
- Worst case: r ≈ 2 for alphabet size 129
- Best case: r = 1 for power-of-2 alphabets
Space Complexity
- Node.js: O(128n) for pool (amortized O(1) per ID)
- Browser: O(n) per ID
Benchmark Results
nanoid() 3,693,964 ops/sec
customAlphabet() 2,799,255 ops/sec (24% slower due to rejection)
nanoid/non-secure 2,226,483 ops/sec (no crypto overhead)Security Properties
Unpredictability
- Node.js: Uses OS-level CSPRNG (Cryptographically Secure PRNG)
- Browser: Uses Web Crypto API (hardware-backed when available)
- No seed: Cannot predict future IDs from past IDs
Uniformity
Every character has exactly equal probability:
P(char) = 1 / alphabet.length
For urlAlphabet (64 chars):
P(char) = 1/64 = 0.015625 (exactly)See Security for detailed analysis.
What’s Next?
- Security - Cryptographic properties
- Performance - Benchmarks and optimization
- Custom Alphabets - Design your own alphabets