Skip to Content

Last Updated: 3/9/2026


Security

Nano ID is designed for cryptographic security. This guide explains its security properties and best practices.

Overview

Nano ID provides three core security guarantees:

  1. Unpredictability: Cannot predict future IDs from past IDs
  2. Uniformity: All characters have equal probability
  3. Collision resistance: Extremely low probability of duplicates

Unpredictability

Hardware Random Generation

Nano ID uses cryptographically secure random number generators (CSPRNG):

Node.js:

import { webcrypto as crypto } from 'node:crypto' crypto.getRandomValues(buffer) // Uses OS-level CSPRNG (e.g., /dev/urandom on Linux)

Browsers:

crypto.getRandomValues(new Uint8Array(21)) // Uses Web Crypto API (hardware-backed when available)

Why Not Math.random()?

Math.random() is not cryptographically secure:

// INSECURE: Predictable function insecureId() { return Math.random().toString(36).slice(2) }

Problems:

  • Predictable: Seeded by time, can be guessed
  • Low entropy: Only ~52 bits of randomness
  • Reproducible: Same seed = same sequence

Nano ID is secure:

  • Unpredictable: Uses hardware entropy
  • High entropy: 126 bits for 21-char IDs
  • Non-reproducible: Cannot recreate sequence

Entropy Sources

Node.js uses OS-level entropy:

  • Linux: /dev/urandom (ChaCha20-based)
  • macOS: SecRandomCopyBytes (hardware RNG)
  • Windows: BCryptGenRandom (CNG)

Browsers use:

  • Hardware RNG: Intel RDRAND, ARM TrustZone
  • OS entropy: Falls back to OS CSPRNG
  • Entropy pool: Mixed with user interactions, timings

Uniformity

The Problem with Modulo

A common mistake creates biased distribution:

// WRONG: Non-uniform distribution function biasedId(alphabet) { let id = '' for (let i = 0; i < 21; i++) { const byte = crypto.getRandomValues(new Uint8Array(1))[0] id += alphabet[byte % alphabet.length] // ❌ BIASED } return id }

Why it’s biased (alphabet size 30):

Random bytes: 0-255 (256 values) 256 % 30 = 16 remainder Characters 0-15: appear 9 times (0, 30, 60, ..., 240) Characters 16-29: appear 8 times (16, 46, 76, ..., 226) Bias: First 16 chars are 12.5% more likely!

Nano ID’s Solution: Rejection Sampling

Nano ID uses bitmask + rejection for perfect uniformity:

// Find smallest power of 2 >= alphabet size const mask = (2 << (31 - Math.clz32((alphabet.length - 1) | 1))) - 1 // Map byte to alphabet, reject if out of range const char = alphabet[randomByte & mask] || '' if (char) id += char // Only accept valid chars

Result: Every character has exactly 1/alphabet.length probability.

Collision Resistance

Birthday Paradox

Collision probability follows the birthday problem :

P(collision) ≈ (n² / 2) × (1 / alphabet^size) For default Nano ID (21 chars, 64-char alphabet): P(collision) ≈ (n² / 2) / (64^21) ≈ n² / (2 × 2^126)

Collision Probability Table

IDs GeneratedCollision ProbabilityTime at 1M IDs/sec
1,000~0.000000000000000000000000000000001%1 second
1,000,000~0.0000000000000000000000001%16 minutes
1,000,000,000~0.00000000000000001%11 days
103,000,000,000,000~0.0000001% (1 in billion)3,268 years

Practical interpretation:

To have a 1% chance of collision, you’d need to generate ~2.8 × 10^18 IDs (2.8 quintillion).

Size vs Security

SizeBitsIDs for 1% collisionUse Case
848~16 millionShort codes (check calculator)
1060~1 billionMedium security
21126~2.8 × 10^18Default (UUID equivalent)
32192~1.9 × 10^28High security tokens

Always use the collision calculator  to verify safety for your use case.

Attack Vectors

Timing Attacks

Not vulnerable: Nano ID’s random generation time is constant (doesn’t leak information).

// Time to generate is independent of ID value const id1 = nanoid() // ~2700 ns const id2 = nanoid() // ~2700 ns

Brute Force

Infeasible: For 21-character IDs:

Search space: 64^21 = 2^126 = 85 undecillion At 1 trillion guesses/second: Time to brute force 50% = 2^125 / 10^12 seconds = 1.3 × 10^18 years = 100 million × age of universe

Prediction Attacks

Not vulnerable: CSPRNG output cannot be predicted even with:

  • Past IDs
  • Partial ID knowledge
  • Timing information

Side-Channel Attacks

Partially vulnerable: Like all software, vulnerable to:

  • Cache timing: Theoretical (requires local access)
  • Power analysis: Requires hardware access
  • Spectre/Meltdown: OS/hardware level

Mitigation: Use OS-level CSPRNG (already done).

Best Practices

✅ Do’s

Use default size for security-critical IDs:

const sessionId = nanoid() // 21 chars (126 bits) const apiToken = nanoid(32) // 32 chars (192 bits)

Use secure variant for tokens:

import { nanoid } from 'nanoid' // ✅ Secure const authToken = nanoid()

Check collision probability:

// For 1 billion IDs at size 10: // Use https://zelark.github.io/nano-id-cc/ // Result: ~0.1% collision probability

❌ Don’ts

Don’t use non-secure for sensitive data:

import { nanoid } from 'nanoid/non-secure' // ❌ Not for production const sessionId = nanoid() // ❌ Predictable!

Don’t reduce size without checking:

const id = nanoid(6) // ❌ High collision risk! // For 1 million IDs: ~1.5% collision probability

Don’t use custom random generators unless necessary:

import { customRandom } from 'nanoid' const nanoid = customRandom(alphabet, 21, () => { return Array(21).fill(0).map(() => Math.random() * 256) // ❌ INSECURE })

Vulnerability Reporting

To report security vulnerabilities:

  1. Do NOT open a public GitHub issue
  2. Use Tidelift security contact 
  3. Tidelift will coordinate fix and disclosure

Compliance

FIPS 140-2

Nano ID uses OS-level CSPRNGs which are typically FIPS 140-2 compliant:

  • Node.js: Depends on OpenSSL FIPS module
  • Browsers: Depends on OS crypto implementation

Check your environment:

# Node.js node -p "crypto.getFips()" # Should return 1 if FIPS enabled

GDPR / Privacy

Nano IDs are not personally identifiable:

  • Random generation (no user data)
  • Cannot be reversed to user information
  • Safe to log and store

However, IDs can become PII through association:

// The ID itself is not PII const userId = nanoid() // But this association makes it PII under GDPR db.users.insert({ id: userId, email: 'user@example.com' })

Further Reading

What’s Next?