Why ID Selection Matters
In distributed systems, assigning a unique identifier to every record is a fundamental need. Choosing the wrong ID scheme can lead to database index fragmentation, sorting difficulties, or even privacy leaks. This article systematically compares UUID versions and ULID to help you make the right choice for your scenario.
UUID Basics
A UUID (Universally Unique Identifier) is a 128-bit globally unique identifier, formatted as an 8-4-4-4-12 hex string:
550e8400-e29b-41d4-a716-446655440000
The core value of UUIDs is generating unique IDs without central coordination, making them a cornerstone of distributed systems. The spec has evolved; the latest is RFC 9562 (May 2024, superseding RFC 4122).
Version Details
UUID v1: Timestamp + MAC Address
Method: current timestamp (60 bit, 100ns precision) + machine MAC address (48 bit) + clock sequence (14 bit)
Pros:
- Sortable by generation time
- No random number generator needed
Cons:
- Privacy: MAC address exposes the machine's physical location
- Security: generation time and hardware info can be inferred
- Multithreading: clock sequence coordination needed on the same machine
- MAC addresses may be duplicated in virtualized environments
Use case: legacy system compatibility only; not recommended for new projects.
UUID v4: Pure Random
Method: 122 bits of randomness (6 bits for version/variant)
Pros:
- Simple implementation; no timestamp or machine ID needed
- No privacy leakage
- Negligible collision probability (2¹²² space)
Cons:
- Not sortable: completely random, no temporal ordering
- Database index disaster: random inserts cause frequent B+ tree page splits and fragmentation
- 36 bytes when stored as a string
Collision intuition: after generating 2.71 × 10¹⁸ v4 UUIDs, collision probability reaches 50%. At 1 billion IDs/second, that takes ~85 years.
Use case: unique identification where sorting isn't needed—e.g., temporary tokens, message IDs.
UUID v6: Reordered v1
Method: same timestamp + MAC as v1, but timestamp high-order bits first, so lexicographic order matches chronological order.
Position: solves v1's sorting problem but retains the MAC address drawback. Low real-world adoption.
UUID v7: Timestamp + Random (Recommended)
Method: first 48 bits are a Unix millisecond timestamp; remaining 74 bits are random (including version/variant bits)
Pros:
- Time-sortable: lexicographic order equals chronological order—perfect for DB indexes
- High write performance: monotonically increasing; B+ tree appends without page splits
- Distributed-friendly: no machine ID needed; nodes generate independently
- No privacy leak: no MAC address
- UUID-compatible: still a standard 128-bit UUID
Cons:
- Millisecond precision; uniqueness within the same millisecond relies on randomness
- Generation time can be roughly inferred (usually fine, but be aware)
Use case: database primary keys for new projects, distributed event IDs, sortable unique identifiers. The recommended choice in 2025.
UUID v8: Custom
RFC 9562's v8 allows a fully custom 128-bit layout. For special needs like embedding business data. Not recommended for general use.
ULID
ULID (Universally Unique Lexicographically Sortable Identifier) is an alternative to UUID, designed specifically for sortability.
Structure
01ARZ3NDEKTSV4RRFFQ69G5FAV
└── Timestamp (48 bit) ──┘└── Random (80 bit) ──┘
- 128 bits total, same as UUID
- Encoded as 26-character Crockford Base32 string
- Millisecond Unix timestamp
ULID vs UUID v7
| Feature | UUID v7 | ULID |
|---|---|---|
| Length | 128 bit | 128 bit |
| String form | 36 chars (with hyphens) | 26 chars |
| Encoding | Hexadecimal | Crockford Base32 |
| Sortable | By time, lexicographically | By time, lexicographically |
| Case sensitivity | Insensitive | Insensitive |
| URL-friendly | Must handle hyphens | No special characters |
| Standardized | RFC 9562 | Community spec |
| Ecosystem | Native UUID libraries | Requires dedicated library |
| Time precision | Millisecond | Millisecond |
Recommendation
- Prefer UUID v7: if you want to reuse existing UUID infrastructure and DB native types
- Prefer ULID: if you value shorter strings and URL-friendliness, and don't mind an extra dependency
Database Primary Key Deep Dive
Why Random IDs Hurt Index Performance
In MySQL InnoDB, the primary key is the clustered index—data is physically stored in key order. Random UUID v4 inserts mean:
- New rows land at arbitrary positions in the B+ tree
- Frequent page splits cause write amplification
- Index fragmentation means more disk pages read per query
- Buffer pool hit rate drops
UUID v7 / ULID are time-increasing: new rows always append to the end of the index, improving write performance several-fold.
Scheme Comparison
| Scheme | Sortable | Index Perf | Storage | Uniqueness | Scale |
|---|---|---|---|---|---|
| Auto-increment | ✅ | Best | 4-8 bytes | Central allocation | Single machine / small |
| UUID v4 | ❌ | Poor | 16 bytes | Decentralized | Small-medium |
| UUID v7 | ✅ | Excellent | 16 bytes | Decentralized | Any scale |
| ULID | ✅ | Excellent | 16 bytes | Decentralized | Any scale |
| Snowflake | ✅ | Excellent | 8 bytes | Needs machine ID | Large-scale distributed |
Practical Advice
- New projects: prefer UUID v7 or ULID as primary key
- Existing systems: keep the current scheme; migration cost usually isn't worth it
- Ultra-large scale: consider Snowflake (8 bytes, more space-efficient)
- Public APIs: avoid exposing auto-increment IDs (enumerable); use UUID/ULID
Security Notes
Random Number Quality
The random portion of UUID v4 and v7 must use a cryptographically secure RNG (CSPRNG):
- ❌
Math.random(): not crypto-safe, predictable - ❌ Simple PRNGs
- ✅ Node.js:
crypto.randomBytes() - ✅ Python:
secretsmodule (notrandom) - ✅ Java:
SecureRandom - ✅ Go:
crypto/rand
Don't Use UUIDs as Security Tokens
UUIDs are designed for uniqueness, not unpredictability. Session tokens, CSRF tokens, and API keys should use dedicated token schemes (e.g., 256-bit random + HMAC).
v1 Privacy Risk
UUID v1 contains the MAC address; attackers can infer:
- The machine's physical network location
- The approximate generation time
- Whether IDs came from the same machine
Never use v1 in new systems.
Language Library Recommendations
| Language | UUID v4 | UUID v7 | ULID |
|---|---|---|---|
| JS/TS | uuid |
uuid (v9.0+) |
ulid |
| Python | uuid (stdlib) |
uuid-utils |
python-ulid |
| Java | java.util.UUID |
com.github.f4b6a3:uuid-creator |
ulid-creator |
| Go | google/uuid |
github.com/google/uuid (v1.6+) |
oklog/ulid |
| Rust | uuid crate |
uuid crate (v1.10+) |
ulid crate |
JavaScript Example
import { v4, v7 } from 'uuid';
import { ulid } from 'ulid';
const idV4 = v4(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
const idV7 = v7(); // '018f6b22-9c2a-7c8b-9e3f-4a5b6c7d8e9f'
const ul = ulid(); // '01ARZ3NDEKTSV4RRFFQ69G5FAV'
Python Example
import uuid
from ulid import ULID
id_v4 = uuid.uuid4()
id_v7 = uuid.uuid7() # Python 3.14+
ul = str(ULID())
Decision Tree
Need a globally unique ID?
├── Need time-based sorting?
│ ├── Want standard UUID format? → UUID v7 ✅
│ └── Want shorter strings? → ULID ✅
├── No sorting needed?
│ └── UUID v4 (general purpose)
└── Need max performance and short ID?
└── Snowflake (requires machine ID allocation)