# UUID v4 vs ULID: Which Should You Use for IDs?

When you need a unique identifier that you can generate anywhere — on the client, in a worker, across services — without coordinating with a central authority, you reach for a random ID. The two most common choices are **UUID v4** and **ULID**. They solve the same problem and look superficially similar, but one of them is sortable and the other will quietly fragment your database index.

## What each one is

A **UUID v4** is 128 bits, almost entirely random, rendered as 36 characters with hyphens:

```
f47ac10b-58cc-4372-a567-0e02b2c3d479
```

A **ULID** is also 128 bits, but it's split into two parts: a 48-bit millisecond timestamp followed by 80 bits of randomness. It's rendered as 26 characters using Crockford's Base32 (no hyphens, no ambiguous letters like `I`, `L`, `O`, `U`):

```
01ARZ3NDEKTSV4RRFFQ69G5FAV
└─ time ──┘└─ randomness ─┘
```

Both fit in the same 128 bits, so both have effectively zero collision risk for any realistic workload. The difference is what those bits *encode*.

## The key difference: sortability

A UUID v4 has no internal order. Two UUIDs generated a second apart are no more "adjacent" than two generated a year apart — they're random points in a 128-bit space.

A ULID leads with a timestamp, so **lexicographic sort order matches creation order**. Sort a list of ULIDs as strings and you get them back in roughly the order they were created (down to the millisecond; within the same millisecond, the random component breaks ties). That single property is why people pick ULID.

## Why random IDs hurt your database

This is the part that bites teams in production. If you use a UUID v4 as a **primary key** in a B-tree-indexed table (the default in MySQL/InnoDB and most setups), every insert lands at a random position in the index. The database constantly splits pages and writes to scattered locations, which:

- thrashes the buffer pool (the page you need is rarely cached),
- causes page splits and fragmentation,
- inflates write amplification on disk.

A ULID (or any time-ordered ID) inserts in **near-append order** — new rows cluster at the "end" of the index, pages fill sequentially, and the working set stays hot. On large, write-heavy tables this is a measurable throughput difference, not a micro-optimization.

If you're stuck with UUIDs and InnoDB, **UUID v7** — a newer time-ordered UUID variant — gives you the same sortability benefit while staying in the UUID format. It's worth knowing about, but ULID has had wider library support for longer.

## A quick comparison

| | UUID v4 | ULID |
|---|---|---|
| Bits | 128 | 128 |
| Length | 36 chars (with hyphens) | 26 chars |
| Sortable by time | no | yes |
| URL-safe | needs care (hyphens ok) | yes (Base32) |
| Index-friendly inserts | no | yes |
| Reveals creation time | no | yes (timestamp is readable) |
| Ubiquity / tooling | everywhere | good, slightly less |

## When to use which

- **Reach for ULID** when the ID is a database primary key, when you want naturally chronological ordering "for free," or when shorter, URL-friendly identifiers matter.
- **Stick with UUID v4** when you specifically *don't* want to leak creation time (a ULID's timestamp is plainly readable), when a downstream system mandates UUID format, or when you value maximum ecosystem compatibility over sortability.
- **Consider UUID v7** if you want ULID's ordering but need to stay in the UUID format for an existing schema or library.

## Generate some and see

The fastest way to build intuition is to generate a batch of each and look at them side by side — notice how a column of ULIDs sorts cleanly while UUIDs scatter. The [UUID/ULID generator](/tools/security/uuid-generator) produces both in bulk, entirely in your browser, so you can copy a set straight into a seed script or test fixture.

The short version: same 128 bits, but ULID spends the first 48 of them on a timestamp — and that one decision is what makes it sortable and index-friendly.
