· articles · 4 min read

By Ankit Jain

What 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.

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:

  1. Read the current Unix timestamp in milliseconds
  2. Generate 80 bits of cryptographically strong randomness
  3. Concatenate timestamp + randomness
  4. 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: 01HZY9N2Z7J6F4E4QYB1QX9D2K

For 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

AspectULIDUUID v4
Sortable by timeYesNo
Length26 chars36 chars
Binary size128 bits128 bits
Index localityGoodPoor
Human readabilityHigherLower
GenerationLocalLocal

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.

DatabaseTypical StorageNotes
PostgreSQLCHAR(26) or BYTEAExtensions available
MySQLCHAR(26)Index-friendly
MongoDBStringNo native ULID type
SQLiteTEXTWorks well for small datasets
SQL ServerCHAR(26)Custom generation
CassandraTEXTPartition key friendly
RedisStringIdeal for ordered keys
CockroachDBSTRINGGood fit with distributed SQL

Further Reading: When not to use ULIDs

[Top]

Back to Blog