UUID vs ULID: Which Should You Use for Database IDs?

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/1042 tells a competitor exactly how many orders you've processed. IDs like /orders/550e8400-e29b-41d4-a716-446655440000 reveal 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 and crypto.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 uuid type 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_at column
  • 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

Frequently Asked Questions

What is a UUID?

UUID stands for Universally Unique Identifier. It is a 128-bit label used to uniquely identify records in computer systems. UUID v4 is the most common variant — fully randomly generated. A UUID v4 looks like 550e8400-e29b-41d4-a716-446655440000. There are approximately 5.3 × 1036 possible values, making collisions negligible in any real-world system.

What is the difference between UUID and ULID?

UUID v4 is fully random — 122 bits of randomness in a 36-character hyphenated string. ULID uses 48 bits for a millisecond timestamp followed by 80 bits of randomness, encoded as a 26-character URL-safe string. The critical difference: ULIDs are lexicographically sortable by creation time, which makes them significantly better for database primary keys where insertion order affects index performance.

Is UUID v4 cryptographically secure?

Yes. UUID v4 uses a CSPRNG — crypto.getRandomValues() in browsers and crypto.randomBytes() in Node.js. The randomness is suitable for session tokens, API keys, and secret identifiers, as long as the full UUID is treated as opaque and never truncated.

Why is ULID better for database primary keys?

ULIDs are time-ordered, so new rows insert at the end of the B-tree index rather than at random positions. Random inserts (UUID v4) cause index fragmentation — the database engine splits and reorganizes pages continuously, increasing I/O at scale. ULIDs behave like sequential IDs from an index perspective while remaining globally unique and generator-free.

Can I use UUID as a primary key in PostgreSQL or MySQL?

Yes. PostgreSQL has a native uuid type. MySQL stores UUIDs as CHAR(36) or BINARY(16) (binary is more efficient). The trade-off is index performance at large scale: UUID v4 random insertion causes B-tree fragmentation with high write throughput. For most applications, UUID v4 is perfectly fine. For tables with millions of rows and sustained high writes, prefer ULID or UUID v7.