· articles · 4 min read
By Ankit JainWhat is ULID?
ULID is a lesser-known but highly practical identifier format for distributed systems. This article breaks down its internal structure, ordering guarantees, and trade-offs compared to UUID, with a system-design lens for architects.

Unique identifiers sit at the foundation of almost every distributed system you design. They show up in primary keys, message IDs, log correlation, event sourcing streams, and replication protocols. For years, UUID has been the default answer to this problem. It works, but it also carries constraints that become visible once you operate at scale or care about time-ordering.
ULID, short for Universally Unique Lexicographically Sortable Identifier, is an alternative that keeps the uniqueness guarantees of UUID while adding deterministic ordering and better operational characteristics. This post walks through ULID from a technical and architectural perspective, focusing on how it behaves under real system workloads.
What is ULID?
A ULID is a 128-bit identifier designed to be:
- Globally unique
- Sortable by creation time using simple string comparison
- Safe to generate in distributed environments without coordination
Unlike UUID v4, which is entirely random, ULID encodes time directly into the identifier. This makes it suitable for systems where write order, temporal queries, or storage locality matter.
At a high level, a ULID is composed of:
- A 48-bit timestamp in milliseconds since Unix epoch
- An 80-bit randomness component to avoid collisions within the same millisecond
These two parts are encoded using Crockford’s Base32 into a fixed-length, 26-character string.
Origin
ULID was introduced in 2016 by Alizain Feerasta as a response to practical issues seen with UUIDs in databases and distributed logs. UUID v4 provides strong uniqueness but no meaningful ordering. UUID v1 includes time, but it leaks MAC addresses and is rarely allowed in security-conscious systems.
ULID sits between these approaches. It encodes time without embedding host identifiers, and it remains sortable without extra indexes or transformations.
For architects, the key motivation is simple: you get time-ordered identifiers without a central ID service.
ULID Structure in Detail
::contentReference[oaicite:0]{index=0}A ULID is always 26 characters long. Internally, it maps to 128 bits.
Timestamp Component (48 bits)
- Represents milliseconds since
1970-01-01T00:00:00Z - Supports over 281 trillion unique timestamps
- Covers roughly 10,889 years of range
From a system design angle, this timestamp enables:
- Natural ordering in append-only stores
- Efficient range scans for time-based queries
- Reduced need for secondary indexes on
created_at
Randomness Component (80 bits)
- Ensures uniqueness for IDs generated in the same millisecond
- Allows up to 1.2e24 combinations per millisecond
- Makes collision probability negligible even under high concurrency
Unlike Snowflake-style IDs, this randomness does not encode shard or worker identity. That choice simplifies generation but shifts ordering guarantees slightly under concurrency, which we’ll discuss later.
Encoding Format
ULID uses Crockford’s Base32 encoding:
- Character set excludes ambiguous characters (I, L, O, U)
- Case-insensitive, but typically rendered in uppercase
- Lexicographical order matches chronological order
This encoding is optimized for logs, URLs, filenames, and human inspection.
How ULID Generation Works
ULID generation is a local operation. No coordination, no shared counters, no clock synchronization beyond standard system time.
The process is:
- Read the current Unix timestamp in milliseconds
- Generate 80 bits of cryptographically strong randomness
- Concatenate timestamp + randomness
- Encode the 128-bit value using Base32
Decoding reverses this process and allows you to extract the timestamp portion, which is useful for debugging and analytics.
Example in JavaScript
const { ulid } = require('ulid');
const id = ulid();
console.log(id);
// Example: 01HZY9N2Z7J6F4E4QYB1QX9D2KFor architects, this means:
- IDs can be generated at the edge, in clients, or inside services
- No dependency on database sequences or ID services
- No write amplification caused by random key distribution
ULID vs UUID: Comparison
| Aspect | ULID | UUID v4 |
|---|---|---|
| Sortable by time | Yes | No |
| Length | 26 chars | 36 chars |
| Binary size | 128 bits | 128 bits |
| Index locality | Good | Poor |
| Human readability | Higher | Lower |
| Generation | Local | Local |
From a database perspective:
- ULIDs reduce B-tree fragmentation compared to random UUIDs
- Inserts tend to be append-heavy rather than scattered
- Time-based queries can leverage primary key order
UUID v4 still makes sense when ordering leaks are undesirable or when strict randomness is preferred.
Database Support for ULID
Most databases do not have native ULID types, but support them through strings or binary fields.
| Database | Typical Storage | Notes |
|---|---|---|
| PostgreSQL | CHAR(26) or BYTEA | Extensions available |
| MySQL | CHAR(26) | Index-friendly |
| MongoDB | String | No native ULID type |
| SQLite | TEXT | Works well for small datasets |
| SQL Server | CHAR(26) | Custom generation |
| Cassandra | TEXT | Partition key friendly |
| Redis | String | Ideal for ordered keys |
| CockroachDB | STRING | Good fit with distributed SQL |
Further Reading: When not to use ULIDs