Decomposing ULID

ULID is identifier usually used in database, as alternative to UUID or normal integer. One of main advantage is ULID is sortable; by including the timestamp data in the identifier hence making it sortable when storing in database.

Example, 01JGFJJZ00XHF7E02JJ03AE4T7 is ULID that was generated at 01 Jan 2025 00:00:00.000 UTC. We can break it down as below:

  • First 10 characters (or 48-bit) is 01JGFJJZ00, which is the timestamp portion
  • Next 16 characters (or 80-bit) is XHF7E02JJ03AE4T7, which is the randomness portion

While the randomness portion is randomly generated, I’m interested to know how’s the timestamp portion is produced.

So how’s the timestamp portion is produced?

Let’s dive on the technicality, and get ready for some mental workout.

To convert the datetime to ULID timestamp portion:

  • Convert the datetime to epoch (or UNIX timestamp) milliseconds
  • Convert epoch timestamp to binary number
  • Convert the binary number to Crockford’s Base32; which is set of alphanumeric characters of 0 to 9 and A to Z, except I, L, O, U to avoid confusion

Using same example like above; we are using ULID 01JGFJJZ00XHF7E02JJ03AE4T7 in which are generated at 01 Jan 2025 00:00:00.000 UTC.

Below is the representation of the datetime in epoch and binary number of the epoch:

Datetime 01 Jan 2025 00:00:00.000 UTC
Epoch 1735689600000
Binary 11001010000011111001010010111110000000000

Since the total length of the binary bit is 41, we add leading 0s so that the total length of the binary bit is 50 (for easier understanding later).

The updated binary number is 00000000011001010000011111001010010111110000000000; where the highlighted red is leading 0s that we just added.

To convert to Crockford’s Base32, we need to divide the binary number to group of 5-bit (the reason I make it 50 characters previously). And from that group of 5-bit, we will convert to decimal and lastly mapped it to Crockford’s Base32. The example is depicted below:

Group of 5-bit binary 00000 00001 10010 10000 01111 10010 10010 11111 00000 00000
Decimal value 0 1 18 16 15 18 18 31 0 0
Crockford’s Base32 0 1 J G F J J Z 0 0

And below is complete Crockford’s Base32 mapping table for your reference.

Decimal value 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Crockford’s Base32 0 1 2 3 4 5 6 7 8 9 A B C D E F G H J K M N P Q R S T V W X Y Z

Hence, the timestamp portion of ULID which is first 10 characters for timestamp 01 Jan 2025 00:00:00.000 UTC is 01JGFJJZ00

I left some example in PHP as your brain exercise to figure it out:

<?php

$crockford_mapping = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

$timestamp = 1735689600000;
$result = '';

while ($timestamp > 0) {
    $binary = $timestamp % 32;
    $result = $crockford_mapping[$binary] . $result;
    $timestamp = intval($timestamp / 32);
    
    // or you can use bitwise operator
    // another brain exercise for you to figure out why it's same like dividing by 32
    // $timestamp >>= 5;
}

echo str_pad($result, 10, '0', STR_PAD_LEFT); // 01JGFJJZ00