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