UUID vs ULID vs Token Identifier Guide
UUID, ULID and random tokens are the three most common identifier types for developers. This guide compares their format, length, sortability and typical use cases so you can choose the right fit for database keys, API keys and session IDs.
Practical Notes
- v4 is random and therefore unordered; as a primary key it causes page splitsThe 122 random bits scatter inserts across the B-tree, so MySQL and Postgres split pages constantly, hurting write throughput and fragmenting storage. Write-heavy tables should use v7 (time-ordered prefix) or ULID instead.
- A UUID is not automatically unguessablev1 embeds the MAC address and timestamp, from which the machine and generation time can be derived; v4 is random but only cryptographically strong if produced by a CSPRNG rather than Math.random(). Anything used as a token or secret must come from Web Crypto (randomUUID or getRandomValues).
- Storage form changes both size and query costStoring 10 million rows as 36-character text costs roughly 360 MB; a 16-byte BINARY(16) column cuts that to about 160 MB and compares faster. The trade-off is readability in queries and logs, so the usual pattern is binary internally and hyphenated text at the edges.
Data Sources
- RFC 4122 (UUID)
- RFC 9562 (UUID v6 / v7 / v8)
- ULID specification (ulid/spec)
Codes, names and definitions above come from public specifications; we claim no originality over the specification data itself. The arrangement, Chinese annotations and practical notes on this page are written by us.
How It Works
Identifiers balance two seemingly conflicting goals: global uniqueness and sortability/predictability. Early UUID v4 is purely random — unique but leaks no time and is not sortable. When databases need insertion-order scans (to avoid B-tree page splits), time-prefixed schemes appeared: UUID v7 (first 48 bits = Unix ms timestamp) and ULID (first 48 bits = Crockford Base32 timestamp).
Tokens differ in purpose from UUIDs: a UUID is designed for uniqueness and fits primary/object keys; secrets like API keys, sessions and activation codes should prefer cryptographically secure random tokens, with length, alphabet and prefixes (e.g. sk_live_) scoped by permission. Base58 / Base62 drop visually confusing characters (0/O, I/l) for safer manual entry.
When to Use This Table
This table is for choosing an identifier scheme for a system. It places UUID versions, ULID, KSUID, snowflake IDs, ObjectId, tokens and API keys side by side across length, entropy, sortability and typical use, so you do not have to jump between documents while choosing.
When not to consult it: you are making the distributed consistency and availability trade-offs. Whether an ID must increase monotonically, whether it leaks business volume, and how to handle clock drift across regions are architectural decisions, not lookups; the table gives only static property comparisons.
Rule of thumb: first ask whether time ordering matters. If yes, choose a time-ordered scheme (ULID, KSUID, snowflake) — random UUIDv4 is unique but inserts at random positions and hurts B-tree locality. If no, UUIDv4 is the simplest option with native cross-language support and no coordination cost.
Mind that entropy is not strength: the length/entropy column reports bit width, not resistance to guessing. How strong a 128-bit value is depends on whether it came from a cryptographically secure random source; a UUID assembled from Math.random is predictable no matter how many bits it claims.
Boundaries and Common Pitfalls
Generating UUIDv4 from Math.random weakens both uniqueness and security
Math.random yields a pseudo-random sequence with a period around 2^53 whose state can be inferred by an observer. At scale it collides readily (duplicates show up in the millions), and a UUID assembled from it is not a security primitive either. Use a cryptographically secure source instead: crypto.randomUUID() or crypto.getRandomValues(new Uint8Array(16)) in the browser, crypto.randomUUID() or crypto.randomBytes(16) in Node.
UUIDv1 leaks MAC address and timestamp; v4 and v7 are the current choices
v1 is assembled from a timestamp plus a node identifier which is usually the MAC address, so anyone holding a v1 ID can infer its creation time and machine details. v6 and v7, introduced by RFC 9562, are the modern replacements: v7 puts the millisecond timestamp in the high bits, keeping uniqueness and monotonicity without exposing the MAC. Rule of thumb: need ordering ⇒ v7 (or ULID); otherwise v4; and never use v1 for externally visible IDs.
Exposing raw identifiers to clients leaks business volume
Auto-increment or time-ordered IDs encode the rule that total equals maximum, so a competitor scraping your endpoints can derive order volume, user counts and growth curves. Regulators increasingly treat such identifiers as personal data under GDPR-like rules. The standard approach is an opaque public ID while the sequential primary key stays internal, or simply UUIDv4/ULID so the identifier carries no volume information.
Clock drift across regions breaks ordering in time-based schemes
ULID and KSUID depend on the local clock, and NTP skew between data centres can make timestamps go backwards, breaking the assumption that ID order equals time order and even causing key collisions. Snowflake separates nodes by machine bits and tolerates small drift logically, but drift beyond the rollback window still collides. Mitigations: synchronise and monitor the clock offset at startup, refuse allocation when it is too large, and keep a monotonic counter as a fallback.
How This Relates to Neighbours
To generate random identifiers or passwords, use the in-site UUID and random tools; this page is a selection comparison, not a generator.
For encoding identifiers in URLs, filenames or database keys, the JSON encoding and charset pages are closer; this page only covers scheme choice.
Protocol details and lifecycle management for OAuth, JWT and API keys belong to authentication system design, not to a lookup table.
If the goal is debugging duplicate-ID incidents, property comparisons here will not localise the problem; you need the actual generation code and logs.
Practical Snippet
Key points: (1) generate UUIDs with crypto.randomUUID or getRandomValues; Math.random makes 128 bits of claimed entropy meaningless and collides at scale; (2) version rule — ordering needed ⇒ v7 or ULID, otherwise v4, and never v1 for externally visible IDs since it leaks MAC and creation time; (3) time-ordered schemes depend on the clock, so cross-region NTP drift breaks the assumption that ID order equals time order and the offset must be monitored; (4) public IDs must be opaque, otherwise scraping your endpoints reveals business volume and may trigger personal-data compliance duties; (5) hand-rolled time-ordered UUIDs easily get the version and variant nibbles wrong — use a library in production.
// ① 生成:必须用密码学安全源
const id = crypto.randomUUID() // ✅ 浏览器/Node 18+
// ❌ Math.random 拼出来的 UUID:可预测、批量下会碰撞
const bad = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,
c => (Math.random() * 16 | (c === 'y' ? 4 : 0)).toString(16))
// ② 自己拼 v4(需要控制格式时)
function uuidV4() {
const b = crypto.getRandomValues(new Uint8Array(16))
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // variant 10
const h = [...b].map(x => x.toString(16).padStart(2, '0')).join('')
return [h.slice(0, 8), h.slice(8, 12), h.slice(12, 16), h.slice(16, 20), h.slice(20)].join('-')
}
// ③ 时间有序:v7 / ULID(需按时间排序时选这类)
const v7 = crypto.randomUUID().replace(/^(.{8})-(.{4})-(.{4})/, (m, a, b, c) => {
const ts = BigInt(Date.now()).toString(16).padStart(12, '0')
return ts.slice(0, 8) + '-' + ts.slice(8, 12) + '-' + '7' + c.charAt(0) + '-' + m.slice(19)
})
// 说明:生产环境请直接用 uuid@13+ 或 ulid 库,别手搓时序
// ④ 熵 ≠ 强度:检查生成源
console.log(typeof crypto.randomUUID()) // 'function' ⇒ 安全源
console.log(typeof Math.random) // 'function' ⇒ 伪随机,不要用于 ID
// ⑤ 对外不透明、内部有序:用映射表把外部 ID 换成主键
// const publicId = nanoid() // 外部可见
// const row = await db.byPublicId(publicId) // 内部仍用自增主键| Identifier | Format | Length / Entropy | Sortable | Best For |
|---|---|---|---|---|
| UUID v4 | xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx | 122 random bits + 6 version/variant bits | No (fully random) | Database keys, distributed IDs, privacy-sensitive IDs |
| UUID v7 | Timestamp (48 bits) + random (74 bits) | 128 bits | Yes (monotonically increasing by time) | New systems needing sortable primary keys |
| ULID | TTTTTTTTTTRRRRRRRRRRRRRRRR (Crockford Base32) | 48-bit timestamp + 80-bit randomness | Yes (lexicographically sortable by generation time) | URL-safe, sortable, human-readable identifiers |
| Random Token | Hex / Base64 / Base62 string | As needed (commonly 128–256 bits) | No | API keys, sessions, activation codes, invite codes |
Frequently Asked Questions
Can UUIDs collide?
UUID v4 collision probability is negligible: you would need to generate roughly 10^18 122-bit random UUIDs before expecting a single collision, far beyond normal business scale. UUID v7 and ULID add a time prefix; IDs generated in the same millisecond still have 80 bits of randomness, so collision risk remains negligible.
Should I choose UUID v4 or ULID?
Choose ULID when you need sortability, shorter URL representation and better readability. Choose UUID v4 for maximum compatibility and when you do not want to leak generation time. For new systems needing time-sortable primary keys, consider UUID v7 directly.
Which alphabet should a token use?
Hex is simplest and most readable; Base64 is shortest but includes +/= which can break URLs; Base62 strips special characters, balancing length and URL safety; Base58 also removes visually confusing characters (0/O, I/l), ideal for manual entry.
Should an API Key be a UUID or a random token?
API keys should use a cryptographically secure random token (at least 128 bits), not a raw UUID. UUIDs are designed for uniqueness; while they provide enough entropy, dedicated tokens give you control over length, alphabet and prefixes (e.g. sk_live_xxx), making permission scoping easier.