If you’ve worked with UUIDs, you probably love their uniqueness — and hate their randomness.
Enter ULID: a lexicographically sortable, URL-friendly, and high-performance alternative to UUIDs. In distributed systems, where sort order and readability matter, ULIDs can be a huge win.
Let’s take a closer look at what they are, how they work, and where you might want to use them.
🔍 What Is a ULID?
ULID stands for Universally Unique Lexicographically Sortable Identifier. It was introduced by Alizain Feerasta in 2016 as an improvement over UUIDv4, addressing two major limitations:
- Lack of natural sort order
- Low human readability
A ULID is a 128-bit identifier — just like a UUID — but structured in a way that makes it sortable by creation time.
ULID Format (128 bits total):
- 48 bits for timestamp (in milliseconds since Unix epoch)
- 80 bits for randomness
Example ULID:
01H5ZYPW4ZYZNM8B8X7TPZBZV7
🧪 Why ULID Instead of UUID?
✅ Lexicographical Sortability
ULIDs are designed so that newer IDs are greater than older ones when sorted as strings.
This is perfect for:
- Logs
- Time-series data
- Row-level insert order
- Pagination without external timestamps
✅ URL-safe, No Hyphens
ULIDs are encoded in Crockford's Base32, making them:
- Shorter than UUID strings
- Easier to copy/paste
- Safe for URLs and filenames
✅ Client-Side Friendly
You can generate ULIDs in the browser, in the DB, or on edge nodes — no need for coordination or clocksync with a central server.
⚖️ ULID vs UUID: A Quick Comparison
Feature | UUIDv4 | ULID |
---|---|---|
Uniqueness | ✅ Very high | ✅ Very high |
Sortable | ❌ No | ✅ Yes |
Time Encoded | ❌ | ✅ (48-bit millis) |
URL-safe | ❌ (hyphens) | ✅ |
Encoding | Hex | Crockford Base32 |
Human-friendly | ❌ | ✅ |
Native DB Support | ✅ (Postgres) | ❌ (store as CHAR) |
Random bits | 122 bits | 80 bits |
🧰 When to Use ULIDs
ULIDs shine in systems where time-based order matters but you still want global uniqueness.
🔄 Great for:
- Event-sourced systems
- Append-only logs
- Audit trails
- Message queues (e.g., Kafka keys)
- Primary keys where sort order improves performance
🚫 Avoid if:
- You need cryptographic-level randomness (UUIDv4 might be better)
- You're relying on native DB UUID support
- Millisecond granularity isn’t precise enough (use Snowflake-style IDs)
💻 ULID in Code
Python
from ulid import ULID
id = ULID()
print(str(id)) # e.g. 01H5ZYPW4ZYZNM8B8X7TPZBZV7
Install with:
pip install ulid-py
JavaScript
import { ulid } from 'ulid';
const id = ulid();
console.log(id); // e.g. 01H5ZYPW4ZYZNM8B8X7TPZBZV7
Go
import "github.com/oklog/ulid/v2"
id := ulid.Make()
fmt.Println(id.String())
🧬 Internals: How It Works
- The timestamp ensures sortability
- The random component ensures uniqueness within the same millisecond
- The final result is 26 characters long
ULID Collision Risk?
It’s astronomically low.
With 80 bits of randomness per millisecond, ULIDs can safely generate millions per node per second without fear of collision.
🗃️ ULIDs in Databases
PostgreSQL
- No native ULID type
- Store as
CHAR(26)
orBYTEA
- Index performance is good if consistently generated
MySQL
- Use
CHAR(26)
orBINARY(16)
- Consider prefix indexing for faster queries
> Tip: ULIDs work best when you don’t truncate or pad inconsistently
🔮 ULID vs KSUID vs Snowflake
ULID isn’t alone. Here’s how it stacks up:
Format | Sortable | Size | Base | Designed for |
---|---|---|---|---|
ULID | ✅ | 26 | Base32 | General usage |
KSUID | ✅ | 27 | Base62 | High-entropy tracing |
Snowflake | ✅ | 64-bit | Base10 | Centralized, Twitter |
UUIDv7 | ✅ | 36 | Hex | RFC standard |
📈 Adoption in the Wild
- PlanetScale uses ULIDs for row-level replication
- FaunaDB and Firestore explored ULIDs for key generation
- Many startup stacks adopt ULIDs for ID-as-primary-key strategies
🔐 Are ULIDs Secure?
ULIDs aren’t cryptographically secure, but they are:
- Random enough for most systems
- Impossible to guess without observing generation
- Safe to expose in public APIs
If you need tamper-proof IDs, use JWTs, signed tokens, or CIDs instead.
Final Thoughts
UUIDs got us far — but for many use cases, ULIDs are a better default.
They’re globally unique, time-sortable, URL-safe, and pleasant to work with.
If you’re tired of uuid.uuid4()
giving you unpredictable chaos in your logs and databases, maybe it’s time to give ULIDs a spin.
🧭 The future of identifiers might just be base32.