Last Updated: 3/9/2026
Security Details
A deep dive into how Nano ID ensures unpredictability and uniformity.
Overview
Nano ID provides cryptographically strong random IDs through:
- Unpredictable random number generation (hardware RNG)
- Uniform distribution (no bias in character selection)
- Well-documented implementation (auditable code)
Random Number Generation
Node.js
Nano ID uses the crypto module’s randomBytes() function:
import crypto from 'crypto'
const bytes = crypto.randomBytes(size)This function:
- Uses the operating system’s cryptographically secure random number generator (CSPRNG)
- Sources entropy from
/dev/urandomon Unix-like systems - Uses
CryptGenRandomon Windows - Leverages hardware random number generators when available (Intel RDRAND, etc.)
Browsers
Nano ID uses the Web Crypto API:
const array = new Uint8Array(size)
window.crypto.getRandomValues(array)This API:
- Is implemented by all modern browsers
- Uses platform-specific CSPRNGs
- Provides the same security guarantees as Node.js crypto
Why Not Math.random()?
❌ Never use Math.random() for IDs:
// ❌ INSECURE - Don't do this!
function badId() {
return Math.random().toString(36).substring(2)
}Problems with Math.random():
- Predictable - Uses deterministic algorithms (usually xorshift128+)
- Seedable - Can be reverse-engineered if you know a few outputs
- Not uniform - Has bias in certain ranges
- Not cryptographic - Never intended for security
Real-world risk: An attacker who observes a few IDs can predict future IDs and potentially hijack sessions, guess database keys, or forge tokens.
Uniform Distribution
The Modulo Bias Problem
A common mistake when building ID generators:
// ❌ BIASED - Don't do this!
function biasedId(alphabet, size) {
let id = ''
for (let i = 0; i < size; i++) {
const randomByte = getRandomByte() // 0-255
id += alphabet[randomByte % alphabet.length]
}
return id
}Problem: If alphabet length doesn’t divide evenly into 256, some characters appear more often.
Example: Alphabet with 10 characters (0-9)
- Characters 0-5 appear 26 times in range 0-255
- Characters 6-9 appear 25 times in range 0-255
- 6% higher probability for 0-5!
This reduces entropy and makes brute-force attacks easier.
Nano ID’s Solution
Nano ID uses an unbiased algorithm:
// Simplified version of Nano ID's approach
function uniformId(alphabet, size) {
const mask = (2 << Math.log(alphabet.length - 1) / Math.LN2) - 1
const step = Math.ceil(1.6 * mask * size / alphabet.length)
let id = ''
while (true) {
const bytes = crypto.randomBytes(step)
for (let i = 0; i < step; i++) {
const byte = bytes[i] & mask
if (byte < alphabet.length) {
id += alphabet[byte]
if (id.length === size) return id
}
}
}
}How it works:
- Creates a bitmask that’s the next power of 2 above alphabet size
- Generates random bytes and masks them
- Rejects masked values >= alphabet length
- Ensures every character has exactly equal probability
Visual Proof
Nano ID’s distribution is tested and proven uniform:

Chi-squared test showing uniform distribution across all characters
Security Guarantees
Unpredictability
✅ Impossible to predict future IDs even if you know previous ones
Nano ID uses cryptographically secure random sources. Each ID is independent and unpredictable.
Example: Even if an attacker knows 1 million of your IDs, they cannot predict the next one.
Collision Resistance
✅ Astronomically low collision probability
With default settings (21 characters, 64-character alphabet):
- 126 bits of entropy (similar to UUID v4’s 122 bits)
- Need to generate ~103 trillion IDs for 1-in-a-billion chance of collision
- At 1000 IDs/second, would take 3.2 million years to reach 1% collision risk
No Information Leakage
✅ IDs reveal nothing about your system
Unlike sequential IDs or timestamp-based IDs:
- Can’t determine when ID was created
- Can’t estimate total number of records
- Can’t guess nearby IDs
- Can’t determine which server generated it
Comparison with Other Methods
UUID v4
Similarities:
- Both use cryptographic randomness
- Similar collision probability (122 vs 126 bits)
- Both unpredictable
Nano ID advantages:
- Shorter (21 vs 36 characters)
- URL-friendly (no encoding needed)
- Smaller bundle size (118 vs 423 bytes)
Sequential IDs (Auto-increment)
// ❌ INSECURE for public use
let id = 0
function getNextId() {
return ++id // Predictable!
}Problems:
- ❌ Fully predictable
- ❌ Reveals record count
- ❌ Enables enumeration attacks
- ❌ Requires coordination in distributed systems
Use sequential IDs only for:
- Internal-only databases
- Where predictability is acceptable
- Single-server systems
Timestamp-based IDs (ULID, Snowflake)
// Timestamp + random
function timestampId() {
return Date.now().toString(36) + nanoid(10)
}Trade-offs:
- ✅ Sortable by creation time
- ⚠️ Reveals creation time (information leakage)
- ⚠️ Slightly less entropy in random portion
Use timestamp IDs when:
- You need time-ordered IDs
- Creation time isn’t sensitive
- You want range queries by time
Hashes (SHA, MD5)
// ❌ WRONG - Hashing doesn't create unique IDs
function hashId(data) {
return crypto.createHash('sha256')
.update(data)
.digest('hex')
.substring(0, 21)
}Problems:
- ❌ Not unique (hash collisions possible)
- ❌ Depends on input data
- ❌ Predictable if input is known
Hashes are for integrity, not uniqueness.
Vulnerabilities and Reporting
Known Non-Issues
“IDs can be guessed”
- ✅ False. Cryptographically random = unpredictable
- With 126 bits of entropy, guessing is computationally infeasible
“Collisions are possible”
- ✅ True, but probability is negligible
- Same as UUID v4 (industry standard)
- Use the collision calculator to verify safety
“No checksums”
- ✅ By design. IDs are for uniqueness, not integrity
- If you need integrity, use HMAC or digital signatures
Reporting Security Issues
To report a security vulnerability:
- Do not open a public GitHub issue
- Use the Tidelift security contact
- Tidelift will coordinate disclosure with maintainers
Best Practices
✅ Do
import { nanoid } from 'nanoid'
// Use default settings for maximum security
const sessionToken = nanoid()
// Use longer IDs for high-security scenarios
const apiKey = nanoid(32)
// Check collision probability for custom sizes
const slug = nanoid(10) // Verify at https://zelark.github.io/nano-id-cc/❌ Don’t
// ❌ Don't use Math.random()
const badId = Math.random().toString(36)
// ❌ Don't use non-secure version for security-critical IDs
import { nanoid } from 'nanoid/non-secure'
const sessionToken = nanoid() // Insecure!
// ❌ Don't make IDs too short without checking collision probability
const id = nanoid(4) // Collisions likely!
// ❌ Don't use IDs as cryptographic keys
const encryptionKey = nanoid() // Use crypto.generateKey() insteadSource Code
Nano ID’s implementation is fully documented:
Main algorithm: index.js
The entire implementation is ~20 lines of code. All “hacks” and optimizations are explained in comments.
Code Audit
Key security-relevant code:
// Mask to avoid modulo bias
let mask = (2 << (31 - Math.clz32((alphabet.length - 1) | 1))) - 1
// Generate enough random bytes
let step = Math.ceil((1.6 * mask * size) / alphabet.length)
// Rejection sampling for uniformity
let id = ''
while (true) {
let bytes = crypto.randomBytes(step)
let i = step
while (i--) {
let byte = bytes[i] & mask
if (byte < alphabet.length) {
id += alphabet[byte]
if (id.length === size) return id
}
}
}Security properties:
- Uses
crypto.randomBytes()(CSPRNG) - Bitmask eliminates modulo bias
- Rejection sampling ensures uniformity
- No branches based on secret data (timing-safe)
Further Reading
- Secure random values (in Node.js) - Excellent article on random generation theory
- Birthday problem - Why collision probability isn’t intuitive
- Cryptographically secure pseudorandom number generator - CSPRNG overview
Related
- Core API - How to use nanoid()
- Comparison with UUID - Detailed comparison
- Collision Calculator - Check your configuration safety