UUID vs ULID: Which Unique ID Should You Use?

21 February, 2026 Backend

UUID and ULID are both 128-bit identifiers used as primary keys and correlation IDs in distributed systems. They look different, behave differently under load, and each has trade-offs the other doesn't. This article covers the technical internals and gives you a clear decision framework.

What Is UUID?

UUID (Universally Unique Identifier) is a 128-bit identifier standardised in RFC 4122. Despite five versions existing, you will almost always encounter UUID v4 - randomly generated, no external dependencies, no central coordinator required.

f47ac10b-58cc-4372-a567-0e02b2c3d479

The format is always 8-4-4-4-12 hexadecimal digits. Bits 48-51 of the raw value encode the version (4), and bits 64-65 encode the variant (10 binary, RFC 4122 variant). The remaining 122 bits are random, giving a collision probability of roughly 1 in 2^61 for a billion IDs per second over 85 years - effectively zero for any real system.

Generate a UUID v4 directly in your browser.

The Other UUID Versions

  • v1 - encodes a MAC address and timestamp. Deprecated in RFC 9562 because it leaks hardware information and is not random enough for security-sensitive use.
  • v3 / v5 - deterministic, derived from a namespace + name via MD5 or SHA-1. Useful for generating a stable ID for a known entity (same input always produces the same UUID).
  • v4 - 122 bits of randomness. The industry default for primary keys.
  • v7 - time-ordered, introduced in RFC 9562. Covered in detail below.

What Is ULID?

ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit ID defined in the ULID community spec. It splits its 128 bits into two parts:

  • 48 bits - Unix timestamp in milliseconds (good until the year 10895)
  • 80 bits - cryptographically random

Both parts are encoded together as 26 characters using Crockford's Base32, which deliberately excludes ambiguous characters (0, O, I, L):

01ARZ3NDEKTSV4RRFFQ69G5FAV
└─── 10 chars ───┘└─── 16 chars ────┘
   timestamp (48 bits)  random (80 bits)

The timestamp prefix makes ULIDs lexicographically sortable: two ULIDs can be compared as plain strings and the one generated later will always sort after the earlier one. Generate a ULID and notice that consecutive IDs share the same first 10 characters if generated within the same millisecond.

Same-Millisecond Monotonicity

If two ULIDs are generated within the same millisecond, the spec requires the random component to increment by 1 to preserve ordering. This guarantees strict sort order even at very high generation rates (thousands per millisecond). The 80-bit random space makes overflow within a single millisecond astronomically unlikely in practice.


UUID v7: The Modern Standard

UUID v7 was standardised in RFC 9562 (April 2024) and combines the time-ordering of ULID with the ubiquity of the UUID format:

018e2b3d-a1c2-7000-b0e5-4a9f1c8d7e2a

It uses a 48-bit Unix millisecond timestamp, 12 version/sequence bits, and 62 random bits. UUID v7 is byte-compatible with UUID v4 - the same 16-byte storage, same uuid column type in PostgreSQL or MySQL, same wire format in every framework. For teams already using UUID infrastructure, upgrading to v7 is often the lowest-friction path to time-ordered IDs.

Generate a UUID v7 to compare.


Side-by-Side Comparison

Feature UUID v4 ULID UUID v7
String length 36 chars 26 chars 36 chars
Encoding Hex Crockford Base32 Hex
Sortable No Yes Yes
Timestamp precision None 48-bit ms 48-bit ms
Same-ms ordering No Guaranteed (spec) Yes (seq bits)
Spec RFC 4122 Community RFC 9562
Native DB type Yes No Yes
Exposes creation time No Yes Yes
Binary storage 16 bytes 16 bytes 16 bytes

Database Performance: Why Random IDs Hurt

Index Fragmentation in B-Tree Indexes

Most relational databases store primary key indexes as B-trees. In MySQL/MariaDB InnoDB, the primary key is a clustered index - table rows are physically stored in primary key order on disk. This means every insert must land in the correct sorted position:

With UUID v4 (random):

  1. The page containing the random ID's position must be loaded into the buffer pool - this is a random read from disk.
  2. If that page is already full, a page split occurs: the page is divided in two, half the rows are moved, and a new page is allocated. Page splits are slow and leave pages half-full.
  3. After millions of inserts, the index is heavily fragmented. Scans that read rows in primary key order jump across many non-sequential pages on disk - random I/O instead of sequential reads.

With ULID or UUID v7 (monotonic):

Every new ID is larger than all previous IDs. Inserts always land at the rightmost leaf of the B-tree, just like AUTO_INCREMENT. Pages fill completely before a new page is allocated. Fragmentation remains near zero.

The performance difference becomes significant at table sizes above ~10 million rows with write-heavy workloads, especially on HDD or under memory pressure where the buffer pool cannot hold the entire index.

PostgreSQL

PostgreSQL does not use clustered indexes by default (heap storage), so the primary key column's B-tree index can still fragment, though the impact is less severe than InnoDB. PostgreSQL also offers the native uuid type (16 bytes) - use it instead of varchar(36) regardless of which UUID format you choose.

Storage: String vs Binary

Storing IDs as strings wastes space and slows comparisons:

Format String storage Binary storage
UUID v4 / v7 36 bytes (varchar) 16 bytes (binary/uuid)
ULID 26 bytes (varchar) 16 bytes (binary)

At 100 million rows, the difference between varchar(36) and binary(16) is over 2 GB on the index alone - before counting row data. For high-volume tables, use BINARY(16) in MySQL or the native uuid type in PostgreSQL.


Security Trade-offs

UUID v4 Is Opaque

UUID v4 reveals nothing about when or where it was generated. A public-facing URL like https://app.example.com/orders/f47ac10b-58cc-4372-a567-0e02b2c3d479 gives an attacker no information about when the order was placed or how many orders exist.

ULID and UUID v7 Expose Creation Time

The first 48 bits of both ULID and UUID v7 encode the Unix millisecond timestamp. Anyone with access to the ID can determine - to the millisecond - when the record was created. For internal primary keys never shown to users, this is irrelevant. For public-facing resource URLs or API responses, consider whether exposing the creation timestamp is acceptable for your use case.

This is not a vulnerability in itself, but it is a privacy consideration worth weighing for your specific context.


Code Examples

PHP

// UUID v4 and v7 - symfony/uid is included with Symfony framework
$uuid4 = \Symfony\Component\Uid\Uuid::v4()->toRfc4122();
// f47ac10b-58cc-4372-a567-0e02b2c3d479

$uuid7 = \Symfony\Component\Uid\Uuid::v7()->toRfc4122();
// 018e2b3d-a1c2-7000-b0e5-4a9f1c8d7e2a

// ULID - symfony/uid also supports ULID
$ulid = new \Symfony\Component\Uid\Ulid();
echo (string) $ulid; // 01ARZ3NDEKTSV4RRFFQ69G5FAV

// Store as binary(16) in MySQL
$binaryUlid = $ulid->toBinary();

Python

import uuid

# UUID v4
uuid4 = str(uuid.uuid4())
# 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

# UUID v7 - requires Python 3.13+ or the 'uuid6' package
# pip install uuid6
import uuid6

uuid7 = str(uuid6.uuid7())
# '018e2b3d-a1c2-7000-b0e5-4a9f1c8d7e2a'

# ULID - pip install python-ulid
from ulid import ULID

ulid = ULID()
str(ulid)          # '01ARZ3NDEKTSV4RRFFQ69G5FAV'
ulid.timestamp()   # 1469918176.385 (Unix seconds as float)
ulid.milliseconds  # 1469918176385

JavaScript / TypeScript

// UUID v4 - native in all modern browsers and Node.js 15+
const uuid4 = crypto.randomUUID();
// 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

// ULID - install the 'ulid' package
import { ulid, decodeTime } from 'ulid';

const id = ulid();
// '01ARZ3NDEKTSV4RRFFQ69G5FAV'

// Extract creation timestamp from a ULID
const timestamp = decodeTime(id);
// 1469918176385 (Unix ms)

SQL: Schema Examples

-- PostgreSQL: native uuid type, no conversion needed
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- MySQL/InnoDB: binary(16) for efficient clustered index
CREATE TABLE events (
    id BINARY(16) NOT NULL,
    payload JSON,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

Extracting the Timestamp

ULID's Base32 encoding makes timestamp extraction readable without library code:

const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

function decodeUlidTime(ulid) {
    return ulid.slice(0, 10)
        .split('')
        .reduce((ts, char) => ts * 32 + ENCODING.indexOf(char), 0);
}

decodeUlidTime('01ARZ3NDEKTSV4RRFFQ69G5FAV');
// 1469918176385 → "2016-07-30T22:56:16.385Z"

UUID v7 timestamp extraction requires a bit more work because the timestamp is split across the standard UUID byte layout, but any UUID v7 library handles it.


When to Use Each

Choose UUID v4 when:

  • Maximum ecosystem compatibility is required (legacy systems, third-party integrations expecting the RFC 4122 format)
  • You need opaque IDs that do not expose creation timestamps in public-facing URLs
  • You are migrating an existing system where changing the ID format carries high risk and cost
  • Simplicity matters more than write performance for your scale

Choose ULID when:

  • Building a new system with no legacy UUID constraints
  • You want shorter, case-insensitive, human-readable IDs for URLs and log files
  • Your team wants to decode creation time from IDs without a database query
  • You prefer community-driven specifications with wide language support

Choose UUID v7 when:

  • You want time-ordered IDs but your infrastructure already handles UUIDs natively (column types, ORMs, REST clients)
  • You prefer IETF-standardised formats over community specs
  • You are already using Symfony, Laravel, or another framework with built-in UUID v7 support
  • You need the B-tree performance benefits with zero schema changes from UUID v4

Conclusion

For new projects, UUID v7 is the pragmatic choice: it is an IETF standard, drop-in compatible with UUID infrastructure, and fixes the database fragmentation problem. ULID is a strong alternative if you value its shorter, more readable format.

For existing projects already using UUID v4: unless you have a measured write performance problem at scale, the migration cost rarely justifies switching. UUID v4 is perfectly adequate for most systems.

Avoid UUID v1, v3, and v5 unless you have a specific deterministic or namespace-based use case.

Try all three: UUID v4, UUID v7, and ULID - all run client-side, no data is sent to a server.

More Articles

UUID Versions Explained: v1, v3, v4, v5, v6, and v7

A complete technical breakdown of all UUID versions. Covers time-based, name-based, and random UUIDs, with code examples in PHP, Python, and JavaScript, and a practical guide to choosing the right version.

28 February, 2026

Password Security and Entropy: Why Length Beats Complexity

A technical guide to password entropy for developers. Covers entropy calculation, character sets, passphrases vs random strings, brute force and rainbow table attacks, and secure password generation.

26 February, 2026

RAG Document Assistant: Answer Questions from Your Own Docs with Ollama, ChromaDB and Docker

Build a local RAG document assistant that reads .txt files, indexes them with vector embeddings, and answers questions using a local LLM — all without a cloud API. Includes a FastAPI backend, a minimal browser UI, and a full Docker Compose setup.

26 February, 2026