Every database row, API resource, and user record needs an identifier. The question is which format to use - and the answer matters more than most developers realize at the start of a project.
Choose wrong and you end up with sequential integers leaking record counts to users, non-sortable UUIDs breaking log analysis, or collision risks in distributed systems. The three main contenders - UUID, ULID, and CUID - each solve a different set of problems, and understanding those tradeoffs upfront saves painful migrations later.
This guide covers how each format works, where it shines, and where it fails. You can generate examples with the UUID generator to follow along.
UUID: The Industry Standard
UUID (Universally Unique Identifier) is defined by RFC 4122 and has been the default unique ID format for decades. A UUID looks like this:
`
550e8400-e29b-41d4-a716-446655440000
`
That is 128 bits of data, formatted as 32 hexadecimal characters split into five groups by hyphens.
UUID Versions That Matter
UUID v4 is the most common - it is randomly generated, making collisions statistically impossible (a 50% collision probability would require generating 2.71 quintillion UUIDs). If you need a UUID today with no special requirements, generate a v4.
UUID v7, finalized in 2024, adds a timestamp prefix, making v7 UUIDs sortable by creation time. This is a significant advantage for database indexes:
`
018e6c5e-d2a3-7000-9e00-a1b2c3d4e5f6
`
With v7, records inserted in sequence have UUIDs that sort in insertion order, which dramatically improves index performance on most databases compared to random v4 IDs. The UUID generator can produce both v4 and v7 in bulk.
UUID Weaknesses
- Not sortable (v4): random UUIDs fragment B-tree indexes, causing slower writes at scale
- Verbose: 36 characters including hyphens is long for URLs and logs
- Exposes no information: a feature for security, but sometimes you want creation time embedded
- Case inconsistency:
550e8400and550E8400represent the same UUID, which causes bugs if not normalized
ULID: Sortable and URL-Safe
ULID (Universally Unique Lexicographically Sortable Identifier) was designed to fix UUID's sortability problem while staying URL-friendly:
`
01ARZ3NDEKTSV4RRFFQ69G5FAV
`
That is 26 characters, entirely uppercase alphanumeric, using Crockford's Base32 alphabet (which excludes visually ambiguous characters like 0, O, I, and L).
How ULID Works
A ULID is 128 bits split into two parts:
- 48 bits: Unix timestamp in milliseconds (the leftmost 10 characters)
- 80 bits: random data (the rightmost 16 characters)
Because the timestamp is the most-significant portion, ULIDs generated in sequence sort lexicographically in creation order. A list of ULIDs, sorted alphabetically, is also sorted by creation time. This makes ULIDs excellent for:
- Database primary keys where index fragmentation is a concern
- Event log identifiers where chronological ordering matters
- Pagination cursors where the ID encodes position
- Distributed system event IDs where multiple services generate IDs without coordination
Multiple ULIDs generated within the same millisecond increment the random portion monotonically, preventing collisions while preserving sort order within the same millisecond burst.
ULID Weaknesses
- Less universal: not an RFC standard, so library support is thinner than UUID
- Timestamp exposure: the creation time is embedded and readable - for user IDs visible in URLs, this can be an unwanted information leak
- 26 characters: shorter than UUID's 36, but still verbose for compact URLs
**ULID (Universally Unique Lexicographically Sortable Identifier)** was designed to fix UUID's sortability problem while staying URL-friendly: ``` 01ARZ3NDEKTSV4RRFFQ69G5FAV ``` That is 26 characters, entirely uppercase alphanumeric, using Crockford's Base32 alphabet (which excludes visually ambiguous characters like `0`, `O`, `I`, and `L`).
CUID and CUID2: Readable and Collision-Resistant
CUID (Collision-Resistant Unique ID) takes a different approach, designed specifically for horizontal scaling and client-side generation:
`
clh7x92k60000k0rlat9j4vgg
`
CUID2 (the current version) produces a fixed-length, random-looking string:
`
tz4a97hwbe8f4cgjsmre7nq4
`
What Makes CUID2 Different
CUID2 uses SHA-512-based randomness to generate a fixed-length identifier with no embedded timestamp:
- 24 characters by default (configurable shorter or longer)
- No embedded timestamp: unlike ULID, CUID2 leaks no creation time
- No hyphens: clean for use in URLs and filenames
- Starts with a letter: valid as a CSS class name or HTML ID attribute without escaping
- Cryptographically strong: uses the Web Crypto API in browsers,
cryptoin Node.js
The original CUID embedded a counter, timestamp, and host fingerprint to prevent collisions across machines. CUID2 simplified this by relying entirely on cryptographic randomness, which achieves the same collision resistance with a cleaner output.
CUID Weaknesses
- Not sortable by creation time: CUID2 IDs do not sort chronologically, unlike ULID
- JavaScript-first: excellent support in JS/TS, but implementations in other languages vary in quality
- Smaller ecosystem: most database tutorials assume UUID, so you may encounter tooling friction
- CUID1 is deprecated: if you encounter old CUID IDs starting with
c, migrate - CUID1 has known collision weaknesses at very high generation rates
Which Format Should You Choose?
The right answer depends on three factors: sortability needs, privacy requirements, and ecosystem constraints.
Use UUID v4 When
- You need maximum compatibility - every database, ORM, and language has first-class UUID support
- The ID will not be used as a database primary key at high write volumes
- You are working with an existing system that already uses UUID
- You want a zero-dependency solution (
crypto.randomUUID()is built into modern browsers and Node.js)
Use UUID v7 When
- You want UUID compatibility but with sortable IDs
- You are starting a new project and can choose freely
- Index performance on large tables is a concern
- You want an embedded timestamp in a standards-compliant format
UUID v7 is often the best default for new projects today - it combines universal UUID support with ULID's main advantage.
Use ULID When
- Sortable IDs are critical and you are not constrained to UUID
- You are building event-sourcing systems, log pipelines, or audit trails
- Your stack has solid ULID library support
Use CUID2 When
- IDs appear in user-facing URLs and you want shorter, cleaner strings
- Privacy matters and you do not want creation time embedded
- You are building a TypeScript-heavy stack
- The ID needs to be a valid CSS class or HTML
idwithout escaping
A Practical Decision Tree
`
New project with no constraints?
└── UUID v7 (sortable, compatible, standardized)
Existing UUID system? └── Keep v4 for consistency
Event log or audit trail? └── ULID (chronological sort is native)
User-facing URLs in a JS/TS stack? └── CUID2 (short, private, valid identifiers)
Privacy-sensitive IDs?
└── CUID2 or UUID v4 (no timestamp embedded)
`
Whatever format you choose, keep it consistent within a system. Mixing UUID and ULID primary keys across tables creates joins requiring type casting and breaks tooling that expects a single ID format.
Use the JSON formatter to inspect API responses and verify which ID format your systems are actually generating, and the API request builder to test endpoints that accept or return different ID formats.
The right answer depends on three factors: **sortability needs**, **privacy requirements**, and **ecosystem constraints**.
Frequently Asked Questions
Can I store ULIDs and CUIDs in a UUID column?
Yes, with caveats. A ULID is 128 bits - the same size as a UUID - so it can be stored as binary in a UUID column. However, most UUID columns expect the hyphenated xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format, and ULIDs use a different character set. The practical approach is to store them as CHAR(26) or TEXT unless you explicitly convert to/from binary UUID representation.
Do UUIDs actually cause database performance problems?
UUID v4 can cause B-tree index fragmentation because insertions happen at random positions rather than at the end of the index. This matters most at high write volumes - millions of inserts per day - on databases like PostgreSQL and MySQL. At moderate scale it is rarely a bottleneck. UUID v7 and ULID solve this by making the index effectively append-only. If you observe slow inserts on a UUID-keyed table, switching to UUID v7 is one of the first optimizations worth profiling.
Is it safe to expose these IDs in public URLs?
All three formats are safe against enumeration attacks - you cannot guess other valid IDs by incrementing a value. ULID leaks creation timestamps, which may be acceptable (a blog post URL) or unacceptable (a user profile URL) depending on context. CUID2 and UUID v4 leak no information beyond the ID itself. The hash generator can create deterministic opaque identifiers from sensitive inputs when you need IDs derived from existing data without exposing that data.
How do I generate ULIDs in a browser or Node.js?
The ulid npm package is the standard choice. Install with npm install ulid, then call ulid() to generate an ID. For browser-native UUID v4 generation, crypto.randomUUID() requires no dependencies and works in all modern browsers and Node.js 14.17+. CUID2 is available via the @paralleldrive/cuid2 package.
Should I use integer auto-increment IDs instead?
Integer IDs are faster and more compact, but they leak record counts (a user with ID 1042 signals roughly 1000 users exist), prevent distributed ID generation without a coordination service, and expose sequential patterns that enable enumeration attacks. For most web applications, UUID v4 or v7 is the better default. Reserve integer IDs for internal-only tables never exposed in URLs or API responses, and where write performance at extreme scale is a measured bottleneck.
JSON Explained: Formatting, Validating, and Converting for Developers
A comprehensive guide to JSON: syntax rules, common errors, formatting tools, JSON Schema validation, and converting between JSON and CSV.
Base64, URL Encoding & HTML Entities Explained
Encode and decode Base64, URLs, and HTML entities instantly. Learn when to use each format, with examples and free converter tools.
Regular Expressions for Beginners: A Practical Guide
Learn regular expression fundamentals, from basic syntax and character classes to practical patterns for matching emails, URLs, and phone numbers.
