Skip to Content
ReferenceSecurity Details

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:

  1. Unpredictable random number generation (hardware RNG)
  2. Uniform distribution (no bias in character selection)
  3. 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/urandom on Unix-like systems
  • Uses CryptGenRandom on 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():

  1. Predictable - Uses deterministic algorithms (usually xorshift128+)
  2. Seedable - Can be reverse-engineered if you know a few outputs
  3. Not uniform - Has bias in certain ranges
  4. 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:

  1. Creates a bitmask that’s the next power of 2 above alphabet size
  2. Generates random bytes and masks them
  3. Rejects masked values >= alphabet length
  4. Ensures every character has exactly equal probability

Visual Proof

Nano ID’s distribution is tested and proven uniform:

Nano ID uniformity test

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:

  1. Do not open a public GitHub issue
  2. Use the Tidelift security contact 
  3. 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() instead

Source 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:

  1. Uses crypto.randomBytes() (CSPRNG)
  2. Bitmask eliminates modulo bias
  3. Rejection sampling ensures uniformity
  4. No branches based on secret data (timing-safe)

Further Reading