SAMAST LogoSAMAST

UUID vs ULID: The Ultimate Guide to Unique Identifiers

24 min read
Harsh Patel

When engineering modern applications, the question of how to generate unique identifiers (IDs) for your database records is a foundational architectural decision. Get it right, and your system scales horizontally with ease. Get it wrong, and you might face catastrophic database performance degradation, fragmented indexes, and painful data migrations down the line.

For decades, the auto-incrementing integer (e.g., 1, 2, 3...) was the undisputed standard. But as systems shifted from monoliths to microservices, and databases moved from single instances to globally distributed clusters, the centralized auto-increment approach became a severe bottleneck. Developers needed IDs that could be generated anywhere—on a mobile client, in a serverless function, or across a distributed database cluster—without risking collisions.

This necessity gave rise to the Universally Unique Identifier (UUID), which became the de facto standard. However, as UUIDs became ubiquitous, their structural flaws—particularly regarding database indexing and chronologic sorting—spawned a new generation of identifiers, most notably the Universally Unique Lexicographically Sortable Identifier (ULID).

In this ultimate, comprehensive guide, we will embark on a deep technical exploration of UUIDs and ULIDs. We will dissect their binary anatomy, explore the internal mechanics of B-Tree database indexes, benchmark their performance, analyze collision probabilities using the Birthday Paradox, and provide implementation examples in Node.js, Python, Go, and Rust. We will also examine alternatives like NanoID and Twitter Snowflake, and show you how to securely manage these identifiers using SAMAST's suite of 100% client-side, privacy-first developer tools.


1. The Core Problem: Why Did We Abandon Auto-Incrementing Integers?

Before we can appreciate the genius of UUIDs and ULIDs, we must understand the environment that made them necessary.

In a traditional monolithic application backed by a single relational database (like MySQL or PostgreSQL), generating an ID is trivial. The database maintains a sequence counter. When you insert a new row, the database assigns the next available integer.

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL
);

The Limitations of Centralized IDs

As your application scales, this centralized approach breaks down for several reasons:

  1. The Single Point of Failure and Bottleneck: If your database is the only entity allowed to mint IDs, every single write operation must go through the primary database node. In a high-throughput system processing thousands of transactions per second, this lock-and-increment mechanism becomes a severe performance bottleneck.
  2. Distributed Systems and Active-Active Replication: Imagine you have two data centers, one in New York and one in London, both accepting writes. If both databases independently assign ID 100 to a new user, you have a massive collision problem when those databases try to sync. You could use odd/even sequences (New York generates 1, 3, 5; London generates 2, 4, 6), but this becomes incredibly complex to manage as you add more nodes.
  3. Information Disclosure (The German Tank Problem): If a user creates an account and gets ID 1050, they instantly know your application has exactly 1,049 other users. If they create an order and get ID 5000, they know exactly how many orders your business processes. Auto-incrementing IDs leak valuable business intelligence to competitors and malicious actors.
  4. Client-Side Generation: In offline-first mobile apps or complex front-end architectures, clients often need to generate an ID before communicating with the server. If the client has to wait for a network round-trip to the database just to get an ID, the user experience suffers drastically.

To solve these issues, the industry needed an identifier that was globally unique, completely decentralized, and virtually impossible to guess.


2. The Reign of UUID (Universally Unique Identifier)

The UUID was standardized by the Open Software Foundation (OSF) as part of the Distributed Computing Environment (DCE) in the 1990s, and later formalized by the IETF in RFC 4122.

A UUID is a 128-bit number. When represented as a string, it is formatted as 32 hexadecimal characters, divided by four hyphens into five groups, following the pattern 8-4-4-4-12.

Example: 123e4567-e89b-12d3-a456-426614174000

The Anatomy of a UUID

While a UUID looks like random noise, it contains specific structural metadata. Let's break down the 128 bits:

  • TimeLow (32 bits): The low field of the timestamp.
  • TimeMid (16 bits): The middle field of the timestamp.
  • TimeHiAndVersion (16 bits): Contains the 4-bit "version" number and the high field of the timestamp.
  • ClockSeqHiAndReserved (8 bits): Contains the 1-to-3 bit "variant" and the high field of the clock sequence.
  • ClockSeqLow (8 bits): The low field of the clock sequence.
  • Node (48 bits): Traditionally the MAC address of the machine generating the UUID.

If you look at the 13th character of a standard UUID, you will always see its version number. In the example 123e4567-e89b-12d3-a456-426614174000, the 1 in 12d3 tells us this is a UUID version 1.

The Evolution of UUID Versions

The UUID standard defines several "versions," each with a different method for guaranteeing uniqueness.

UUIDv1 (Mac Address & Date-Time)

UUIDv1 generates uniqueness by combining the computer's MAC address with the current timestamp (measured in 100-nanosecond intervals since October 15, 1582—the adoption of the Gregorian calendar).

  • Pros: Guaranteed unique across different machines.
  • Cons: It explicitly leaks the MAC address of the generating server, which is a massive security and privacy risk. Because of this, UUIDv1 is heavily discouraged in modern applications.

UUIDv2 (DCE Security)

An obscure version that includes POSIX UID/GID information. It is almost never used in modern software development.

UUIDv3 (MD5 Hash) and UUIDv5 (SHA-1 Hash)

These are "Name-Based" UUIDs. You provide a namespace (which is itself a UUID) and an arbitrary name (like a URL or a username). The algorithm hashes the namespace and the name together (using MD5 for v3, and SHA-1 for v5) to produce the UUID.

  • Pros: Deterministic. Hashing the exact same namespace and name will always yield the exact same UUID.
  • Cons: Useless for general record creation where you need random, non-deterministic IDs.

UUIDv4 (Randomness)

This is the undisputed king of UUIDs, powering almost every modern application. UUIDv4 abandons timestamps and MAC addresses entirely in favor of pure, cryptographic randomness. Of the 128 bits, 6 bits are reserved for version and variant metadata, leaving 122 bits of pure randomness.

  • Pros: Utterly decentralized, zero privacy leakage, completely unguessable.
  • Cons: As we will see later, pure randomness is a nightmare for database performance.

The New Era: UUIDv6, v7, and v8

In recent years, the IETF drafted RFC 9562 to introduce new UUID versions designed specifically to fix the database sorting issues of UUIDv4. UUIDv7 (which uses a Unix Epoch timestamp prefix followed by randomness) is highly regarded as the successor to UUIDv4, heavily inspired by ULID.

Testing UUIDs Securely

If you are generating UUIDs for database seeding, testing API endpoints, or configuring infrastructure, you shouldn't rely on random sketchy websites that might log your newly minted IDs.

SAMAST provides a 100% client-side UUID Generator. This tool uses your browser's native Crypto.getRandomValues() API to securely generate UUIDv4s directly in your local memory. Nothing is ever sent to a server.

SAMAST Secure UUID Generator

Generate highly secure, cryptographically random UUIDv4s instantly in your browser. Perfect for database seeding and API testing without risking data leaks.

Generate Secure UUIDs Offline

The Math: Understanding Collision Probability

When developers first encounter UUIDv4, their immediate concern is: "If there is no central database checking for uniqueness, what happens if two servers randomly generate the exact same UUID?"

The answer lies in the mind-boggling scale of 122 bits of randomness. $2^$ represents $5.3 \times 10^$ possible combinations.

To put this into perspective using the Birthday Paradox (the mathematical theory used to calculate collision probabilities): If you were to generate 1 billion UUIDs every single second for 85 years, the probability of generating a single duplicate would still only be 50%.

For all practical human endeavors, the chance of a UUIDv4 collision is zero. You are statistically more likely to be struck by lightning while simultaneously winning the lottery than you are to experience a UUIDv4 collision in a standard web application.


3. The Great Database Catastrophe: Why UUIDv4 Hurts Performance

If UUIDv4 is completely decentralized, unguessable, and mathematically immune to collisions, why is the industry looking for alternatives?

The problem isn't with the identifier itself; the problem lies in how relational databases store and retrieve data.

B-Tree Indexes Explained

Databases like PostgreSQL, MySQL, and SQL Server organize their primary keys using a data structure called a B-Tree (Balanced Tree). B-Trees are optimized for range queries and extremely fast lookups ($O(\log n)$ time complexity).

For a B-Tree to remain efficient, data must be inserted in a sorted order.

When you use an auto-incrementing integer (1, 2, 3, 4...), every new record is logically larger than the previous one. The database simply appends the new record to the "rightmost" edge of the B-Tree. The data is written sequentially to the disk, which is incredibly fast. The database engine can efficiently cache the most recent pages in RAM, knowing that all new writes will hit those specific "hot" pages.

Index Fragmentation: The Randomness Penalty

Now, consider what happens when you use UUIDv4 as a Primary Key.

Because UUIDv4 is completely, cryptographically random, a newly generated UUID is just as likely to start with an a as it is a 4 or an f. When the database attempts to insert this new UUID into the B-Tree index, it cannot simply append it to the end. It must traverse the entire tree to find the exact middle, left, or arbitrary node where this specific random string belongs lexicographically.

This random insertion pattern causes massive Index Fragmentation:

  1. Page Splits: Database indexes are stored on disk in "pages" (typically 8KB chunks). When a random UUID is forced into the middle of a full page, the database must split that page in two, moving data around on the disk to make room. This is a highly expensive I/O operation.
  2. Cache Misses: Because writes are happening randomly across the entire expanse of the database index, the database cannot efficiently cache the "active" write area in RAM. Every insert is likely to hit a "cold" page that must be fetched from the physical disk, slowing down writes drastically.
  3. Bloat: Fragmented pages lead to unused empty space inside the database files, causing the index to swell in physical storage size far beyond what is necessary.

As your table grows to millions or billions of rows, the penalty of UUIDv4 randomness compounds. Inserts that used to take 2 milliseconds suddenly take 20, 50, or 100 milliseconds. Your Write-Ahead Logs (WAL) bloat, disk I/O spikes to 100%, and horizontal scaling becomes a nightmare.

This database performance crisis was the exact catalyst for the creation of ULID.


4. Enter ULID: The Universally Unique Lexicographically Sortable Identifier

In 2016, Alizain Feerasta proposed the ULID specification. The goal was simple: design an identifier that retains the decentralized, uncoordinated generation benefits of a UUID, but structure it so that it is inherently chronological, thereby solving the B-Tree index fragmentation problem.

A ULID is a 128-bit value, exactly the same binary size as a UUID. However, when encoded as a string, it looks vastly different:

01ARZ3NDEKTSV4RRFFQ69G5FAV

The Anatomy of a ULID

The genius of ULID lies in its internal structure. The 128 bits are sliced into two distinct components:

  1. Timestamp (48 bits): The first 48 bits represent the UNIX timestamp in milliseconds. This gives the ULID chronologic sortability and allows it to represent times up to the year 10889 AD without overflowing.
  2. Randomness (80 bits): The remaining 80 bits are filled with cryptographically secure random data to ensure uniqueness and prevent collisions.

By putting the timestamp at the very front of the identifier, ULIDs are naturally ordered by time. If you generate a ULID right now, and another ULID one millisecond from now, the second ULID will always be lexicographically "greater" than the first.

Why ULID Fixes the Database Problem

Because ULIDs naturally sort by creation time, inserting them into a PostgreSQL or MySQL B-Tree index behaves almost exactly like inserting an auto-incrementing integer. The database simply appends the new ULID to the rightmost edge of the index tree.

  • No massive page splits.
  • No random I/O thrashing.
  • Perfect cache locality.

You get the massive write-speed benefits of sequential integers, combined with the decentralized safety of UUIDs.

Base32 Encoding: Shorter and URL Safe

Another major advantage of ULID is how it is represented as a string. A UUID uses hexadecimal encoding and inserts hyphens, resulting in a 36-character string. ULID uses Crockford's Base32 encoding.

Base32 uses a larger character set (32 characters vs hex's 16), which allows it to pack the exact same 128 bits of data into a shorter, 26-character string.

Furthermore, Crockford's Base32 was explicitly designed to be human-friendly and URL-safe:

  • It excludes the letters I, L, O, and U to avoid visual confusion with numbers (1 and 0) and to prevent the accidental generation of profanity.
  • It is case-insensitive.
  • It contains no hyphens or special characters, meaning you can safely embed it directly in URL paths without triggering encoding issues or breaking regex patterns.

(If you are curious about encoding mechanics and want to convert strings between Base64, Base32, Hex, and ASCII, you can use the SAMAST Base Converter to explore how binary data is transformed into text!)

SAMAST Base Converter Toolkit

Deep dive into encoding! Convert data seamlessly between Base64, Hex, Binary, and Text directly in your browser.

Explore Encoding Tool

Monotonicity: The Same-Millisecond Problem

Astute engineers might ask: "If ULID relies on a millisecond timestamp, what happens if I generate 1,000 ULIDs in the exact same millisecond? Will they still sort correctly?"

The ULID specification elegantly solves this via Monotonicity. When you configure a ULID generator to be monotonic, it detects if a ULID is being generated in the same millisecond as the previous one. Instead of generating 80 completely new random bits, it takes the random bits of the previous ULID and simply increments them by 1.

This mathematical trick guarantees that even if you generate millions of ULIDs inside a single millisecond, they will maintain perfect, predictable chronological order.

SAMAST Lexicographical ULID Generator

Generate chronologically sortable ULIDs instantly. Analyze the exact millisecond timestamp embedded inside them without sending data to a server.

Generate ULIDs Now

5. The Ultimate Showdown: UUID vs. ULID Feature Matrix

To make an informed architectural decision, we must weigh the exact trade-offs of both formats.

Feature / MetricUUIDv4 (Random)ULID (Time-Based)Winner
Binary Size128 bits (16 bytes)128 bits (16 bytes)Tie
String Length36 characters (e.g., 550e8400...)26 characters (e.g., 01ARZ3...)ULID (28% shorter)
URL SafetyRequires stripping hyphens100% URL safe (Alphanumeric)ULID
Sortable by Time?No (Completely random)Yes (Timestamp prefix)ULID
Database Write SpeedSlow (Severe B-Tree fragmentation)Fast (Sequential appending)ULID
Randomness Density122 bits80 bitsUUIDv4
Native DB SupportBuilt-in (Postgres uuid type)Requires coercion to uuid typeUUIDv4
Security (Opacity)True zero-knowledge IDLeaks creation timestampUUIDv4

Deep Dive on Security & Information Disclosure

While ULID dominates on performance, UUIDv4 wins heavily on security opacity. Because a ULID begins with a 48-bit timestamp, anyone who extracts a ULID from your API (e.g., https://api.example.com/users/01ARZ3NDEKTSV4RRFFQ69G5FAV) can decode the first 10 characters to determine the exact millisecond that user account was created.

If you have a business requirement that user creation dates, document generation times, or transaction timestamps must remain completely secret from public view, you cannot use a ULID or a UUIDv7 as your public-facing identifier. You must use the purely random UUIDv4, or hash the ULID before exposing it to the client.

(Do you need to extract timestamps from various formats? Use the SAMAST Date & Time toolkit to parse, format, and calculate epoch offsets securely offline.)

SAMAST Epoch & Timestamp Parser

Easily parse Unix timestamps, manipulate dates, and handle timezone conversions directly in your browser.

Open Date & Time Tools

6. Implementation and Code Examples

Let's look at how you would generate both UUIDs and ULIDs in the most popular backend languages today.

Node.js / TypeScript

Generating UUIDv4: Node.js has a built-in crypto module that generates UUIDs, meaning you don't even need external dependencies anymore!

import { randomUUID } from 'crypto';

const myUuid = randomUUID(); 
console.log(myUuid); // 'e66cd224-b153-48b4-82ee-06b240ff1100'

Generating ULID: You will need the ulid npm package.

npm install ulid
import { ulid, monotonicFactory } from 'ulid';

// Standard ULID
const myUlid = ulid();
console.log(myUlid); // '01ARZ3NDEKTSV4RRFFQ69G5FAV'

// Monotonic ULID (for high-throughput bulk inserts)
const generateMonotonic = monotonicFactory();
const id1 = generateMonotonic();
const id2 = generateMonotonic(); 
// id2 is guaranteed to sort after id1 even if generated in the same ms

Python

Generating UUIDv4: Python's standard library includes the uuid module.

import uuid

my_uuid = uuid.uuid4()
print(my_uuid) # e66cd224-b153-48b4-82ee-06b240ff1100

Generating ULID: You can use the popular python-ulid package.

pip install python-ulid
from ulid import ULID

my_ulid = ULID()
print(my_ulid) # 01ARZ3NDEKTSV4RRFFQ69G5FAV

# You can easily extract the datetime from a ULID in Python!
print(my_ulid.datetime) # 2026-07-05 14:22:15.123456+00:00

Go (Golang)

Generating UUIDv4: The industry standard is Google's UUID package.

import "github.com/google/uuid"

func main() {
    id := uuid.New()
    println(id.String())
}

Generating ULID:

import (
    "math/rand"
    "time"
    "github.com/oklog/ulid/v2"
)

func main() {
    entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
    id := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
    println(id.String())
}

7. Database Best Practices for ULID and UUID

The biggest mistake junior developers make when implementing 128-bit identifiers is storing them as raw VARCHAR strings in the database.

A standard UUID string takes 36 bytes. A ULID takes 26 bytes. However, both of them are natively just 128-bit integers, which take up exactly 16 bytes of space in memory. If you store a 128-bit ID as a string, you are wasting an enormous amount of disk space, inflating your index sizes, and severely slowing down database JOIN operations (string comparison is much slower than binary integer comparison).

Storing in PostgreSQL

PostgreSQL is incredibly advanced and features a native uuid column type. Under the hood, this column stores the ID as highly efficient 16-byte binary data, but it automatically formats it as a 36-character hyphenated string when you query it.

Can you store a ULID in a Postgres uuid column? YES! Because a ULID is exactly 128 bits, it fits perfectly inside a uuid column. However, you must write a small database function or application-layer middleware to decode the 26-character Base32 ULID string into a 36-character UUID hex string before saving it, and encode it back to Base32 when querying. This gives you the ultimate architecture: ULID chronological sorting, Base32 API brevity, and native Postgres binary storage efficiency.

Storing in MySQL

MySQL does not have a native uuid column type. If you are using MySQL, you must use a BINARY(16) column to store UUIDs or ULIDs efficiently.

CREATE TABLE orders (
    id BINARY(16) PRIMARY KEY,
    user_id BINARY(16) NOT NULL,
    total DECIMAL(10,2)
);

You must use the UUID_TO_BIN() and BIN_TO_UUID() helper functions in MySQL 8.0+ to convert your string IDs into binary formats during INSERT and SELECT queries.

MySQL InnoDB Warning: MySQL's InnoDB storage engine uses Clustered Indexes for primary keys. This means the actual physical row data is stored on disk in the exact order of the Primary Key. If you use a random UUIDv4 as a primary key in MySQL InnoDB, the fragmentation penalty is exponentially worse than in PostgreSQL (which uses heap storage). Never use a UUIDv4 as a primary key in MySQL InnoDB. Always use a ULID or a sequential integer.


8. Identifying IDs in JWT Tokens

As you transition your architecture to distributed IDs like ULID or UUID, you will frequently find them embedded as the sub (Subject) claim inside your JWT (JSON Web Tokens) for authentication.

When a user logs in, your server assigns their database ULID as the JWT payload:

{
  "sub": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "role": "admin",
  "exp": 1720195200
}

Debugging these tokens during development can be a hassle, especially since pasting production JWTs into public websites is a massive security vulnerability. With SAMAST, you can instantly decode, parse, and verify JWT signatures 100% offline.

SAMAST Secure JWT Parser

Decode JSON Web Tokens instantly. Verify signatures and inspect payloads (like UUID and ULID subjects) securely in your browser memory. No data is sent to external servers.

Decode JWTs Securely

9. Notable Alternatives in the Ecosystem

While UUID and ULID dominate the enterprise landscape, several alternative ID generation strategies have carved out specific niches. It's crucial to understand them to ensure you are picking the absolute best tool for your job.

1. NanoID

NanoID is a tiny, secure, URL-friendly unique string ID generator created specifically for JavaScript and front-end ecosystems. Unlike UUID, which is strictly 36 characters, NanoID is customizable. By default, it generates a 21-character string using a larger alphabet (A-Za-z0-9_-).

Pros:

  • Up to 60% faster than UUID in Node.js environments.
  • Extremely compact (21 characters vs 36 characters).
  • URL-safe out of the box.

Cons:

  • Not sortable by time (suffers from the exact same database index fragmentation issues as UUIDv4).
  • Custom lengths make database sizing unpredictable if you aren't strict.

If you are generating IDs for short-lived session tokens, temporary cache keys, or NoSQL document stores where B-Tree fragmentation isn't an issue, NanoID is a fantastic, highly performant choice.

SAMAST NanoID Generator

Generate highly compact, URL-safe NanoIDs with customizable lengths and alphabets instantly. Perfect for short-lived tokens and URL slugs.

Generate NanoIDs Offline

2. Twitter Snowflake

Developed by Twitter to solve the massive scale problem of generating unique Tweet IDs in a distributed system, Snowflake IDs are 64-bit integers. They are composed of:

  • 41 bits of timestamp (millisecond precision).
  • 10 bits of machine ID (identifying the specific worker node).
  • 12 bits of sequence number (preventing collisions on the same node in the same millisecond).

Pros:

  • Fits natively into a 64-bit BIGINT database column (only 8 bytes!), making it twice as storage-efficient as UUID/ULID.
  • Chronologically sortable.

Cons:

  • Highly complex to set up. You must configure and manage a Zookeeper or Redis cluster specifically to assign unique "Machine IDs" to all your worker nodes to prevent collisions.
  • JavaScript's Number type cannot safely represent a 64-bit integer, leading to precision loss bugs unless specifically handled as a BigInt or String.

3. CUID and CUID2

Collision Resistant Unique Identifiers (CUID) were designed for horizontal scaling. CUID2 was recently released to fix security vulnerabilities in the original CUID (which leaked MAC addresses and sequential data, similar to UUIDv1). CUID2 relies on strong hashing (Sha3) combined with time, entropy, and process fingerprints.

Pros: highly secure and excellent collision resistance. Cons: Longer strings, not sequentially sortable, and generally slower to generate than ULIDs.


10. The Final Verdict: Which Should You Choose?

We have explored the depths of B-Tree algorithms, collision probabilities, and binary encodings. But at the end of the day, as a system architect or full-stack developer in 2026, you must make a pragmatic decision for your stack.

Here is the definitive guide on what to choose:

The Case for UUIDv4

Use UUIDv4 if:

  1. You are bound by legacy enterprise compliance standards that specifically mandate the RFC 4122 UUID specification.
  2. You require absolute zero-knowledge opacity. If your business logic dictates that a competitor or a user must never be able to extract the creation timestamp of a record from its public URL, UUIDv4 is your only safe choice.
  3. You are building on a database that natively handles random writes well without heavy B-Tree penalties, or your dataset is simply small enough that index fragmentation will never be a bottleneck in your product's lifecycle.

The Case for ULID (The Overall Winner)

Use ULID if:

  1. You are building any new, scale-oriented application today.
  2. You are using PostgreSQL or MySQL and want the lightning-fast, sequential insert performance of an auto-incrementing integer, but need the decentralized, offline-generation capabilities of a UUID.
  3. You are designing a clean REST or GraphQL API and prefer the shorter, URL-safe 26-character Base32 strings over clunky 36-character hyphenated UUIDs.
  4. You want pagination to be deeply optimized. With ULIDs, you can achieve highly efficient "keyset pagination" (e.g., WHERE id > last_ulid LIMIT 50) because the ID itself encodes the chronological order, bypassing the need for a slower ORDER BY created_at clause.

The Case for UUIDv7

As a final note, the IETF has recently standardized UUIDv7. UUIDv7 fundamentally uses the exact same logic as ULID: a timestamp prefix followed by randomness. If your database strictly requires a 36-character string format with hyphens (due to hardcoded ORM validation rules), but you want the database sorting performance of a ULID, UUIDv7 is the perfect middle-ground. However, ULID remains vastly more popular in modern APIs simply due to its cleaner, shorter string representation.


Frequently Asked Questions (FAQ)

To solidify your understanding of decentralized identifiers, here are the most critical questions and edge-case scenarios developers face in production.

Frequently Asked Questions


This guide was brought to you by SAMAST. Ensure your development workflow remains secure, private, and lightning-fast with our suite of 160+ zero-knowledge, client-side tools.