A canonical ULID's timestamp lives in its first 10 characters: those 10 Crockford Base32 digits encode a 48-bit Unix millisecond count that the ULID Generator decodes into both the raw millisecond value and a UTC ISO 8601 instant. A ULID is exactly 26 characters long, written in the Crockford Base32 alphabet that deliberately omits I, L, O, and U to remove visual ambiguity, so the timestamp you read back is computed from the same encoding scheme anyone implementing the canonical ULID specification uses. Decoding is case-insensitive, accepts only canonical characters after normalization, and rejects any input that would exceed the 128-bit total budget of the identifier. Pair the decoded instant with the source row and you have a creation timestamp, an orderable ID, and a deduplication target, all derivable from one 26-character string. The decoder reads characters in the browser only and never posts the value to a remote endpoint, so nothing about the IDs you paste appears in any server log.

decode ulid timestamp
decode ulid timestamp

What a ULID Timestamp Encodes

The ULID layout splits the 128 bits of the identifier into two halves. The first 48 bits, packed into the leading 10 Crockford Base32 characters, carry a big-endian unsigned Unix epoch time in milliseconds. The remaining 80 bits, occupying the last 16 characters, hold the random region used to keep collisions rare. Because each Base32 character holds 5 bits, 10 characters × 5 bits = 50 bits of capacity, which is more than the 48 bits actually used; that slack gives the encoder room to reject any input whose first character is 8 or higher, since the rest of the string would push past 128 bits. The largest timestamp the format can represent is 2^48 - 1, or 281,474,976,710,655 milliseconds, which lands somewhere in calendar year 10,889. Anything above that, including any 26-character canonical string whose first character is 8, 9, or higher, is rejected as overflow rather than wrapped around to zero.

This split is what makes lexical and time order agree: comparing two ULIDs character by character is the same as comparing their millisecond timestamps first, then their random regions. A row whose ULID sorts after another row, byte-wise or under any ASCII-compatible collation, is therefore newer at the millisecond level, unless the 80-bit random region happened to roll over into identical values. That property is the reason ULIDs are popular for ordered indexes, log lines, and event stores.

Why ULIDs Show Their Creation Time

The same 48 bits that let a database scan a B-tree on creation order also let anyone who can read a ULID read its embedded creation time. That is by design. If you generate identifiers for tracing, audit logs, payment events, or distributed traces, exposing an approximate timestamp inside the identifier is usually a feature, because you can sort, join, and dedupe without a separate created_at column. If you generate identifiers for tokens, share links, session IDs that gate access, or anything where the moment of creation is sensitive metadata, the visible timestamp is a liability. Treat the decoded time as an open hint, not as authenticated truth, and never use a ULID where the timestamp alone could disclose something you would normally redact from the row.

Decoding a ULID Timestamp with the ULID Generator

The Decode option on the ULID Generator is the fastest path from a 26-character string to its embedded UTC instant. Everything stays in the current page; no value is uploaded or stored between clicks.

  1. Open the ULID Generator and choose the Decode option from the mode selector at the top of the panel.
  2. Paste a canonical 26-character ULID into the input field. The decoder accepts both uppercase and lowercase letters; mixed case is normalized against the canonical Crockford Base32 alphabet, which excludes I, L, O, and U.
  3. Read the millisecond value the decoder returns. One card shows the raw integer count, and a second card shows the same instant formatted as a UTC ISO 8601 string with millisecond precision and a trailing Z.
  4. Confirm the result visually. If the decoded instant is far from when you generated or received the identifier, suspect a copy-paste error: an extra space, a missing character, a swapped I/L/O/U, or a stray newline is the usual cause.
  5. Switch back to Generate whenever you need a fresh batch. Generation captures one timestamp per click, increments the 80-bit random region monotonically across the 1 to 100 values in that batch, and returns to a fresh random region on the next click.

You can decode as many values as you like in a session, and you can move back and forth between Generate and Decode without losing either side. The two modes never share hidden state across clicks.

Limits the Decoder Catches

The decoder is strict about input so that the time it returns always means the same thing. Four checks run before any number is displayed.

  • Length. The string must be exactly 26 characters. Anything shorter truncates the 128-bit budget; anything longer is not a canonical ULID and is refused.
  • Alphabet. Every character must come from the canonical 32-symbol Crockford alphabet after case folding. The letters I, L, O, and U are excluded for the same reason the timestamp encoding excludes them: 1, I, and l, plus 0 and O, are easy to misread in screenshots and OCR scans.
  • Total bit budget. Because the format is fixed at 128 bits and each character contributes 5 bits, a value whose first character is 8 or higher would need a 130-bit number to represent. Those inputs are rejected as out-of-range rather than silently truncated or wrapped.
  • Timestamp range. The decoded integer must fit inside the 48-bit unsigned range from 0 through 281,474,976,710,655. Inputs whose first 10 characters encode a value above that cap are refused.

If the input fails any of those checks, no timestamp is rendered and the decoder flags the violation in place, waiting for a corrected string. Generation, not decoding, has a separate monotonic overflow check that fails without wrapping when the random region would exceed 80 bits inside one batch.

ULID vs UUIDv4 vs UUIDv7 at a Glance

ULIDs are not the only 128-bit identifiers in use. The table below compares the three formats on the dimensions that matter when you reach for one of them. Properties come from the canonical specifications, not from any single implementation.

PropertyULIDUUIDv4UUIDv7 (RFC 9562)
Canonical length26 characters36 characters (hyphenated)36 characters (hyphenated)
EncodingCrockford Base32HexadecimalHexadecimal
Sortable by timeYes (lexical matches chronological)NoYes (lexical matches chronological)
Embedded timestamp48-bit Unix millisecondsNone (random only)48-bit Unix milliseconds
Random or fixed bits after time80 random bits122 random bits74 random bits plus 6 version/clock bits
Monotonic batch optionBuilt in within one clickNot applicableOptional via sub-millisecond counter
Case-insensitive on decodeYes, after canonical normalizationYes, by designYes, by design
Specificationgithub.com/ulid/specRFC 4122RFC 9562

If you only need an opaque random identifier, UUIDv4 has the largest random region. If you need time-ordered IDs without writing the alphabet yourself, UUIDv7 is the standards-track equivalent of a ULID. If you need short strings that sort cleanly inside a bytewise index and skip the hyphens, ULIDs take less column space and round-trip through copy-paste more reliably.

Verifying a Decode Against the Reference Implementation

When you paste a ULID into a decoder, the result is only as trustworthy as the implementation behind the input field. The ULID JavaScript reference implementation encodes the same Crockford Base32 alphabet, the same 48-bit time field, and the same 80-bit random region that the canonical spec defines, so the timestamps it returns for any canonical string match the values returned by the ULID Generator. If you ever see a divergence between a paste-in result and a written-by-hand decoder, the cause is almost always a non-canonical character: a stray space, a lowercase letter that should have been a digit, an O instead of a 0, or a 1 that lost its serif. Tightening the input usually resolves the discrepancy.

For high-stakes systems, sanity-check the decoder with two or three ULIDs of known origin, including one captured from a sleep across a millisecond boundary and one captured immediately before and after another batch. The decoded instants should match the system clock to within rounding, and successive captures inside one batch should differ only by their random region.

Storing and Sorting ULIDs After You Decode Them

Decoding tells you what time the identifier was created, not how to store it. If you keep the identifier in a database for later ordering, three settings decide whether your queries stay correct. Keep the full uppercase string (or its canonical lowercase form) so bytewise comparisons return the same ordering as time comparisons. Store it in a case-preserving 26-character column, because case-folding, truncation, or an undersized column breaks the agreement between lexical and chronological order. Let the collation behave bytewise or use plain ASCII ordering; locale-sensitive collations that reorder punctuation, numbers, or letters break the property a ULID depends on for fast ordered indexes.

Even with proper storage, treat the 80 random bits as collision-resistant, not collision-proof. Always add a unique constraint on the column and handle a conflict atomically, even though the probability of a duplicate in a single millisecond is vanishingly small. For high-throughput inserts, pair the stored timestamp with the table's monotonic insert path; the ULID Generator's Generate button produces the strict-increment batch the spec describes, where every ID in one click shares the captured millisecond and increments the random field instead of competing for a fresh random draw.

For a deeper look, see How to Convert a Unix Timestamp in SQL Queries.