Auto-increment integers are the default primary key in most tutorials — and they work fine until you need to distribute your database, expose IDs in URLs without leaking business intelligence, or generate IDs client-side before hitting the server. That's where UUID and ULID come in. This guide explains both, compares them directly, and tells you which to choose for your specific situation.
The Problem With Auto-Increment IDs
Sequential integer IDs have real advantages — small, fast, readable — but they create problems:
- Enumerable. A URL like
/orders/1042tells a competitor exactly how many orders you've processed. IDs like/orders/550e8400-e29b-41d4-a716-446655440000reveal nothing. - Not globally unique. In a distributed system with multiple database nodes, sequences collide unless you coordinate them centrally — which creates a bottleneck.
- Can't generate client-side. With auto-increment, the ID only exists after a database INSERT. You can't create an ID in the browser or mobile app before the record is saved.
UUID v4
UUID (Universally Unique Identifier) version 4 is a 128-bit random identifier defined by RFC 4122. It looks like this:
550e8400-e29b-41d4-a716-446655440000
f47ac10b-58cc-4372-a567-0e02b2c3d479
The format is five groups of hex characters separated by hyphens: 8-4-4-4-12. The digit 4 in the third group marks it as version 4. The first hex digit of the fourth group is either 8, 9, a, or b — this marks it as the RFC 4122 variant.
Key properties:
- 122 bits of effective randomness (the remaining 6 bits encode version and variant)
- ~5.3 × 1036 possible values — collision probability is negligible in any realistic system
- Generated with
crypto.getRandomValues()in browsers andcrypto.randomBytes()in Node.js - Universally supported: every database, language, and framework understands UUID v4
- Not sortable. Two UUID v4 values give you no information about which was created first
ULID
ULID (Universally Unique Lexicographically Sortable Identifier) solves UUID's biggest weakness: sort order. A ULID looks like this:
01J3KZQM6N1Y3BWXPFQR8VT5CH
01J3KZQM6N2ATYDFCMHQ3VWXKP
It's a 26-character Crockford Base32 string (no hyphens, URL-safe). Internally it's also 128 bits, but structured differently:
- First 10 characters: 48-bit Unix timestamp in milliseconds
- Last 16 characters: 80 bits of cryptographically secure randomness
Because the timestamp comes first, ULIDs sort lexicographically in creation order. Two ULIDs generated in the same millisecond are still monotonically ordered (the random portion increments).
Side-by-Side Comparison
Feature UUID v4 ULID
─────────────────────────────────────────────────────────────────
Length 36 chars (with hyphens) 26 chars
Bits 128 (122 random) 128 (48 time + 80 random)
Sortable No Yes (millisecond precision)
URL-safe No (contains hyphens) Yes
Time-encoded No Yes
Collision safety Extremely high Extremely high
DB index performance Random inserts → fragmentation Sequential inserts → efficient
RFC standard RFC 4122 ULID spec (github.com/ulid)
Language support Universal Most major languages (library)
Database Performance: Why Sort Order Matters
Most databases use a B-tree structure for primary key indexes. With sequential IDs (auto-increment or ULID), new rows always go to the end of the index — the engine appends to a single location. This is fast and produces compact, well-organized indexes.
With UUID v4, new rows land at a random position in the B-tree. The engine must constantly split pages, reorganize data, and flush more pages to disk. At low volume, the difference is undetectable. At millions of rows and high write throughput:
- Index fragmentation grows over time
- More page reads per query
- Higher disk I/O during writes
- Larger index size on disk
In PostgreSQL benchmarks, UUID v4 primary keys have been measured at 20–30% lower write throughput compared to sequential IDs at high scale. ULID eliminates this entirely while remaining globally unique.
When to Use UUID v4
- Your table will stay under a few million rows
- You need maximum compatibility (older ORMs, legacy tools)
- You're using PostgreSQL's native
uuidtype and the ecosystem built around it - You want session tokens, API keys, or one-time tokens (where randomness is the point and order doesn't matter)
- The ID will never be exposed to users or URLs
When to Use ULID
- Primary keys on high-write tables where index performance matters
- You want to sort records by creation time without a separate
created_atcolumn - You're generating IDs client-side and want a guarantee of insertion order
- You want URL-safe IDs without hyphens
- Distributed systems where you can't use a central sequence
UUID v7: The Option That Bridges Both Worlds
In May 2024, the IETF published RFC 9562, which standardised new UUID versions including UUID v7. It's the most practically important addition: time-ordered like ULID, but in the familiar UUID format — hyphens, 36 characters, native uuid column type in PostgreSQL.
# UUID v7 — time-ordered, looks like a normal UUID
018ffbe4-6e41-7e3a-b9c4-a7e0d3f92e85
018ffbe4-6e42-7c10-a2d1-3f8b9c047e21
↑ millisecond timestamp prefix, always increasing
The structure: 48-bit millisecond timestamp, then version bits (7), then 74 bits of random data. The timestamp comes first, so UUIDs generated later always sort after earlier ones — same B-tree benefit as ULID, zero format change to your schema.
Support is arriving fast: PostgreSQL has pg_uuidv7 as an extension (and native support is discussed for PostgreSQL 18), Laravel 11+ has Str::orderedUuid() which generates UUID v7, and .NET 9 ships Guid.CreateVersion7() built-in. If you're on a greenfield project in 2026 and want sequential IDs in UUID format without any new conventions, UUID v7 is worth evaluating. ULID stays the better pick when you specifically want the shorter, hyphen-free, URL-safe format.
Generating Them in Code
// JavaScript — UUID v4 (native in modern environments)
const id = crypto.randomUUID();
// → "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// JavaScript — ULID (using the 'ulid' package)
import { ulid } from 'ulid';
const id = ulid();
// → "01J3KZQM6N1Y3BWXPFQR8VT5CH"
// PHP — UUID v4 (built-in since PHP 8.1 via Symfony or ramsey/uuid)
use Ramsey\Uuid\Uuid;
$id = Uuid::uuid4()->toString();
// → "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// PHP — manual UUID v4 (no library)
$id = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
Or skip the code entirely — the tool below generates UUID v4 and ULID values instantly in your browser, with bulk generation up to 100 at a time.
Need UUID v4 or ULID values right now? Generate up to 100 at a time, copy-ready.
Try the Free UUID / ULID Generator
CREESOL