NanoID vs UUID vs ULID: Choosing the Right ID for Your Project
5 April, 2026 Backend
I first reached for NanoID when I needed short, URL-safe identifiers for a link shortener. UUIDs were too long, ULIDs still felt verbose, and auto-increment leaked business data. NanoID solved the problem in 21 characters with zero configuration. But it is not always the right choice - here is how the three formats compare and when each one wins.
What Is NanoID?
NanoID is a tiny string ID generator (130 bytes gzipped in JavaScript) that produces URL-safe random identifiers. The default output is 21 characters using an alphabet of 64 URL-safe characters: A-Za-z0-9_-.
V1StGXR8_Z5jdHi6B-myT
Unlike UUID, NanoID has no internal structure - no version bits, no timestamp, no variant field. Every character is pure randomness, which means the full length contributes to collision resistance. The default 21 characters at 64 possible values per character give 126 bits of entropy - comparable to UUID v4's 122 random bits, but in a much shorter string.
Generate a NanoID directly in your browser.
Custom Alphabets and Lengths
NanoID's most distinctive feature is alphabet customisation. You can generate IDs using any character set:
import { customAlphabet } from 'nanoid';
// Numeric-only IDs (e.g. for verification codes)
const numericId = customAlphabet('0123456789', 12);
numericId(); // '839174652031'
// Lowercase + digits only (case-insensitive systems)
const lowerId = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 16);
lowerId(); // 'k7f2m9x4p1b8v3n6'
// Emoji IDs (yes, really)
const emojiId = customAlphabet('πΆπ±ππΉπ°π¦π»πΌ', 6);
emojiId(); // 'πΌπΆπ¦π±π»πΉ'
This flexibility is useful when IDs must conform to external constraints - DNS labels, case-insensitive file systems, human-readable codes, or systems that reject certain characters.
Side-by-Side Comparison
| Feature | NanoID | UUID v4 | ULID | UUID v7 |
|---|---|---|---|---|
| Default length | 21 chars | 36 chars | 26 chars | 36 chars |
| Random bits | 126 | 122 | 80 | 62 |
| URL-safe | Yes | No (- only) |
Yes | No |
| Sortable | No | No | Yes | Yes |
| Timestamp embedded | No | No | Yes (48-bit ms) | Yes (48-bit ms) |
| Custom alphabet | Yes | No | No | No |
| Custom length | Yes | No (fixed 128-bit) | No (fixed 128-bit) | No (fixed 128-bit) |
| Spec | Community | RFC 4122 | Community | RFC 9562 |
| Native DB type | No | Yes (uuid) |
No | Yes (uuid) |
| Exposes creation time | No | No | Yes | Yes |
The standout difference: NanoID is the only format where you control both the length and the alphabet. UUID and ULID are fixed at 128 bits with predefined encodings.
Collision Probability
All random ID generators share the same birthday problem math. The question is always: "How many IDs can I generate before the probability of a collision becomes concerning?"
The Math
For n random IDs with k possible values, the collision probability is approximately:
P β nΒ² / (2 * k)
For NanoID (default 21 chars, alphabet of 64):
- Possible values: 64^21 β 4.7 Γ 10^37
- At 1 million IDs per second, you would need ~1,000 years to reach a 1% collision probability
For UUID v4 (122 random bits):
- Possible values: 2^122 β 5.3 Γ 10^36
- Similar order of magnitude to NanoID default
For ULID (80 random bits per millisecond):
- Possible values per millisecond: 2^80 β 1.2 Γ 10^24
- Much smaller random space, but scoped to a single millisecond - collisions only happen between IDs generated in the same millisecond
In practice, none of these formats have a collision problem at any realistic scale. The database unique constraint is your safety net regardless.
Adjusting NanoID Length
If you shorten NanoID, collision resistance drops fast:
| Length | Entropy (bits) | IDs before 1% collision risk |
|---|---|---|
| 8 | 48 | ~19 million |
| 12 | 72 | ~73 billion |
| 16 | 96 | ~280 trillion |
| 21 (default) | 126 | ~4.6 Γ 10^18 |
For a link shortener with millions of URLs, 12 characters is sufficient. For a distributed system generating billions of events, stick with 21 or longer.
URL Safety and Compactness
NanoID was designed for URLs from the start. Its default alphabet (A-Za-z0-9_-) requires no percent-encoding in any part of a URL - path segments, query parameters, or fragments.
Compare the same ID in a URL:
# NanoID (21 chars, no encoding needed)
/api/orders/V1StGXR8_Z5jdHi6B-myT
# UUID v4 (36 chars, dashes are safe but verbose)
/api/orders/f47ac10b-58cc-4372-a567-0e02b2c3d479
# ULID (26 chars, uppercase but URL-safe)
/api/orders/01ARZ3NDEKTSV4RRFFQ69G5FAV
NanoID is 41% shorter than UUID and 19% shorter than ULID. In contexts where ID length matters - short URLs, QR codes, SMS messages, log lines - this adds up.
Database Considerations
No Native Type
Unlike UUID, which has a native column type in PostgreSQL and MySQL 8, NanoID is stored as VARCHAR. This has implications:
- Storage: A 21-character NanoID in
VARCHAR(21)takes 22-23 bytes (depending on the database's length prefix). A UUID in the nativeuuidtype takes exactly 16 bytes. For tables with hundreds of millions of rows, the difference in index size matters. - Index performance: String comparisons are slower than binary comparisons. The difference is negligible for small tables but becomes measurable at scale.
- No random ordering problem: Like UUID v4, NanoID is fully random and causes B-tree index fragmentation on write-heavy tables. Unlike ULID or UUID v7, there is no timestamp prefix to maintain insert order. If you use MongoDB, ObjectID already embeds a timestamp β NanoID would lose that benefit.
When NanoID Works Well in a Database
- Tables under ~10 million rows where index fragmentation is not a bottleneck
- Systems where the ID is primarily used in application code and APIs, not for range scans
- Cases where a short, URL-safe ID is the primary requirement and you accept the storage trade-off
When to Prefer UUID or ULID
- Write-heavy tables above 10 million rows - use UUID v7 or ULID for time-ordered inserts
- PostgreSQL-centric stacks where the native
uuidtype gives you 16-byte storage and built-in operators - Systems that need to extract creation timestamps from IDs without a database query
Code Examples
JavaScript / TypeScript
// npm install nanoid
import { nanoid, customAlphabet } from 'nanoid';
// Default: 21 chars, URL-safe
const id = nanoid(); // 'V1StGXR8_Z5jdHi6B-myT'
// Custom length
const short = nanoid(12); // 'k7f2m9x4p1b8'
// Custom alphabet + length
const hexId = customAlphabet('0123456789abcdef', 16);
hexId(); // 'a3f8c2e1b9d04f67'
// Async version (uses crypto.getRandomValues internally)
import { nanoid } from 'nanoid';
const asyncId = nanoid(); // same API, async under the hood in some environments
PHP
// composer require hidehalo/nanoid-php
use Hidehalo\Nanoid\Client;
$client = new Client();
// Default 21 chars
$id = $client->generateId();
// 'V1StGXR8_Z5jdHi6B-myT'
// Custom length
$shortId = $client->generateId(size: 12);
// 'k7f2m9x4p1b8'
// Custom alphabet
$client = new Client();
$numericId = $client->formattedId(alphabet: '0123456789', size: 10);
// '8391746520'
Python
# pip install nanoid
from nanoid import generate
# Default 21 chars, URL-safe
id = generate() # 'V1StGXR8_Z5jdHi6B-myT'
# Custom length
short = generate(size=12) # 'k7f2m9x4p1b8'
# Custom alphabet + length
numeric = generate('0123456789', 10) # '8391746520'
NanoID vs UUID vs ULID: When to Use Each
Choose NanoID when:
- You need short, URL-safe IDs and control over length and alphabet
- The ID appears in user-facing URLs, QR codes, or space-constrained contexts
- You are building a JavaScript/TypeScript application where the tiny bundle size matters
- Sortability and embedded timestamps are not required
Choose UUID v4 when:
- Maximum ecosystem compatibility is the priority - every database, ORM, and API understands UUIDs
- You need opaque IDs that reveal nothing about creation time
- You are working with legacy systems that expect RFC 4122 format
- You need time-ordered IDs for database write performance
- Extracting creation timestamps from IDs is valuable for your use case
- You want the B-tree-friendly insert pattern without sacrificing uniqueness
If you want to understand the differences between all UUID versions β including v1, v3, v5, and the newer v7 β or see a deeper comparison of UUID and ULID with database benchmarks and security trade-offs, those articles cover the internals in detail.
Which One to Pick
NanoID fills a gap that UUID and ULID leave open: short, customisable, URL-safe random identifiers. Its trade-off is clear β no timestamp, no native database type, no RFC standard. For public-facing IDs where brevity matters and you do not need time ordering, it is the most practical choice. For everything else, UUID v7 is the safer default.