Skip to content

ULID to Timestamp

A ULID looks like a random string of letters and digits, but it quietly encodes the moment it was created. Paste any ULID and instantly see its embedded creation time, a character-by-character breakdown, and a ready-to-paste query for finding rows by date.

What's inside a ULID?

A ULID — Universally Unique Lexicographically Sortable Identifier — is a 128-bit value, the same size as a UUID, but written as 26 characters instead of 32 hex digits, like 01ARZ3NDEKTSV4RRFFQ69G5FAV. The first 10 characters are a 48-bit Unix millisecond timestamp; the remaining 16 are random. Both parts are encoded with Crockford's Base32, an alphabet that deliberately skips the letters I, L, O, and U to avoid misreadings and awkward accidents — decoders are expected to read a mistyped I or L as 1 and an O as 0.

One consequence of that layout trips people up: the first character can only be 0–7, never 8 or above. Ten Base32 characters can technically hold 50 bits, but the timestamp only needs 48 — so the top two bits are always zero, capping the first character's range. The highest valid ULID, 7ZZZZZZZZZZZZZZZZZZZZZZZZZ, decodes to the year 10889 — so a "ULID won't run out of room" the way a 32-bit timestamp field eventually does.

ULID vs UUID v7 — which should you use?

Both formats solve the identical problem: a 128-bit id that sorts in creation order, so new rows append to the end of a database index instead of scattering across it the way a fully-random id (UUID v4) does. UUID v7 is now an official IETF standard (RFC 9562, ratified in 2024), with native support arriving fast — Postgres 18 ships a built-in uuidv7() function, and it drops into any column or library already typed for a UUID. ULID predates that standard, is 10 characters shorter as a string, and uses a more compact, unambiguous alphabet.

If you're starting fresh in an ecosystem with strong UUID tooling, UUID v7 is the safer default today. If your stack already speaks ULID, or the shorter string form genuinely matters for your use case (URLs, log lines, user-facing ids), there's no strong reason to migrate away from it.

Sort order isn't as strict as you'd think

The ULID spec guarantees monotonic order within the same millisecond by incrementing the random component when a generator produces more than one id in that millisecond — but that counter lives in a single generator's memory. Two processes, or two servers, have no shared counter, so ULIDs minted in the same millisecond on different machines are ordered only by chance. Worse, a server whose clock runs even slightly ahead will mint ULIDs that sort before ones actually created earlier elsewhere.

None of this affects the timestamp this tool decodes — that's read straight from the id, unaffected by any of it. It only matters if your code leans on ULID string order as a strict substitute for a distributed event log, which it isn't.

Finding rows by date without a created_at column

Because the timestamp lives inside the id itself — and a ULID primary key is indexed by default — you can query a date range without a separate created_at column at all. Open the "Generate a ULID for a date-range query" panel above, pick a date, and copy a ready-to-run query for PostgreSQL, Node.js, Python, Java, C#, or PHP.

One caveat: a ULID built this way is for querying only. Its random characters are zero-filled rather than generator-produced, so it isn't guaranteed unique — never insert a row using it as the actual id.

Frequently asked questions

What is a ULID?

A Universally Unique Lexicographically Sortable Identifier — a 128-bit id, like a UUID, but written as 26 characters of Crockford's Base32 instead of 32 hex digits. The first 10 characters are a 48-bit Unix millisecond timestamp; the remaining 16 are random. Because the timestamp leads, ULIDs sort in creation order, which keeps database indexes efficient the way a fully-random id (UUID v4) can't.

How do I extract the timestamp from a ULID?

The first 10 characters encode a Unix millisecond timestamp in Crockford's Base32 alphabet (0123456789ABCDEFGHJKMNPQRSTVWXYZ — no I, L, O, or U). Decode those 10 characters as a base-32 number and you have milliseconds since 1970-01-01. This tool does that instantly and also shows UTC, local time, relative time, ISO 8601, both Unix units, and the same 128 bits reinterpreted as a UUID.

Why does my ULID show "no embedded timestamp"?

Two reasons. First, the value contains a "U" — Crockford's Base32 deliberately excludes that letter (it's easy to misread as V, and skipping it avoids accidentally spelling something unfortunate), so it can't appear in a genuine ULID. Second, the first character encodes a value above 7, which would overflow ULID's 48-bit timestamp field — the highest valid ULID starts with 7 (7ZZZZZZZZZZZZZZZZZZZZZZZZZ, which decodes to the year 10889). Either way, this tool explains which problem it found instead of just rejecting the input.

Is a ULID's timestamp accurate to the microsecond?

No — only to the millisecond. If it looks slightly off from a database created_at column, that's expected: Postgres timestamptz stores microseconds, one order of magnitude finer than a ULID can encode.

Should I use ULID or UUID v7?

Both solve the same problem — a sortable, time-ordered 128-bit id — and either is a reasonable choice today. UUID v7 is now an official IETF standard (RFC 9562, 2024) with growing native support (Postgres 18 ships a built-in uuidv7() function, and most language standard libraries now generate it directly), and it fits any column or library already typed for a UUID. ULID predates that standard, is 10 characters shorter as a string, and uses a more compact, unambiguous alphabet — appealing if you're already on it or want a shorter primary key. If you're starting fresh in an ecosystem with strong UUID tooling, UUID v7 is the safer default; if you value the shorter string or your stack already speaks ULID, there's no strong reason to switch. This site has a matching UUID v7 to Timestamp converter if you need to decode that format too.

Can I trust ULID sort order across multiple servers?

Only loosely. Within a single generator process, ULIDs minted in the same millisecond stay ordered because the spec increments the random component to break ties. But that counter isn't shared across processes or machines — two servers generating ULIDs in the same millisecond have no coordination, and if one server's clock drifts even slightly ahead, its ULIDs will sort before ones actually created earlier elsewhere. Treat ULID order as "very close to creation order," not a strict distributed event log.

Can I query my database by date using just the ULID?

Yes. Because the timestamp is embedded directly in the id, and a ULID primary key is indexed by default, you can query a date range without a separate created_at column at all. Use the range-query generator on this page to get a ready-to-paste query for PostgreSQL, Node.js, Python, Java, C#, or PHP.

Can I create my own ULID with a custom timestamp?

You can, but only for querying — never for inserting a new row. A hand-built ULID's random bits are zero-filled rather than generated, so it isn't guaranteed unique the way a real generator's output is; using it as a real primary key risks a collision with another zero-filled value built for the same millisecond.

Related tools

Smart paste

Paste any timestamp or date, or search for a tool