A canonical ULID encodes its creation time directly inside the identifier itself, so you can read the embedded millisecond timestamp from any 26-character string without calling an external service. The first 10 Crockford Base32 characters hold a 48-bit Unix time in milliseconds, and the final 16 characters hold 80 bits of cryptographic randomness, which gives the format its sortable, monotonic behavior. For bulk operations, the ULID Generator creates 1 to 100 strictly increasing identifiers in a single click using the browser cryptographic random source, and every identifier in that batch shares the same captured millisecond. To read the timestamp from a ULID you already have, the same tool's Decode panel accepts one canonical string at a time and returns the millisecond and the UTC instant. Because generation, decoding, formatting, and copying all happen in the browser, no identifier or timestamp leaves the device.

A canonical ULID carries a 48-bit millisecond timestamp in 10 characters
The ULID format is designed so that the identifier itself sorts the same way time does. A canonical value is always 26 characters long, drawn from a 32-symbol Crockford Base32 alphabet that excludes the letters I, L, O, and U to reduce ambiguity when identifiers are read aloud or transcribed by hand. The first 10 characters represent the time field, and the remaining 16 characters represent the random field. Both regions are concatenated without separators, which is why a ULID looks like one opaque token even though half of it is structured metadata.
| Region | Character count | Bits encoded | Canonical content |
|---|---|---|---|
| Time field | 10 | 48 | Unix milliseconds since the epoch |
| Random field | 16 | 80 | Cryptographic randomness, incremented for monotonic batches |
| Total | 26 | 128 | Sortable, unique, opaque-looking identifier |
The 48-bit time field supports the full range of Unix millisecond timestamps that fit in an unsigned 48-bit integer. The maximum representable time is calculated as:
Formula: 2^48 − 1
Substituted: 281,474,976,710,656 − 1
Result: 281,474,976,710,655 milliseconds past the Unix epoch
That ceiling is well past the year 10,000, so for ordinary development you will never hit the upper limit. Because the identifier uses 128 bits total and 26 Base32 characters carry 130 bits of capacity, the top two bits of the first character must remain clear. That is why a canonical ULID always begins with a value from the set {0, 1, 2, 3, 4, 5, 6, 7} in Base32: any string whose first character would be 8 or higher would need more than 128 bits and is rejected as invalid. This rule is part of the canonical ULID specification, which the JavaScript reference implementation enforces.
How to read ULID timestamps in bulk locally
When the task is bulk, the most useful interpretation is to produce or verify many identifiers whose embedded timestamps you can inspect without a network call. The ULID Generator handles the bulk creation half directly through its Generate panel. For ULIDs you already have, the Decode panel reads one canonical string at a time, so a true bulk decode is a sequence of pastes rather than a single batch operation. The two together cover the typical bulk workflow: generate a fresh batch with a known timestamp, or paste existing identifiers to recover their original timestamps.
- Open the ULID Generator in your browser. No data leaves the page during any of the following steps.
- For a bulk creation, stay on Generate and set a quantity from 1 to 100. Click once to capture a single Date.now() millisecond, fill the 80-bit random region from the browser cryptographic random source, and increment that region by one (with carry) for every additional identifier in the batch.
- Copy the full uppercase strings from the output area. Every identifier in one clicked batch shares the same captured millisecond, so you already know the embedded timestamp without running Decode on each one.
- For an existing ULID, switch to Decode and paste one canonical 26-character string. The panel returns the millisecond and the matching UTC ISO instant, normalized from the time field.
- Repeat the Decode paste for any further identifiers you need to inspect. The Decode action is case-insensitive but only accepts characters from the canonical alphabet after normalization.
- Store any identifiers you keep in a case-preserving field exactly 26 characters wide, and enforce a unique constraint at the database so a rare collision cannot slip through.
Generate produces a strictly increasing batch with one shared timestamp
The Generate panel is built around the specification's monotonic mode. When you click Generate once, the tool reads Date.now() once and treats that single millisecond as the time field for every identifier produced in that click. The 80-bit random field is initialized from the browser cryptographic random source and is then incremented by one for each subsequent identifier in the same batch, with proper carry across the full 80-bit range.
The result is that the returned strings are strictly increasing in normal ASCII lexical order, even though they all encode the same Unix millisecond. This property is what lets a database index ordered by ULID produce records in creation order without an extra secondary sort. If the requested quantity would push the 80-bit field past its maximum, generation fails instead of wrapping, so the tool never produces an out-of-spec identifier. Because the captured millisecond is fresh on every click, a new click always moves the time field forward and never collides with a previous batch on the random region alone.
Decode reads one canonical ULID back to its millisecond and UTC instant
For ULIDs that already exist in logs, queues, or database rows, the Decode panel is the read side of the same tool. You paste a canonical 26-character string, the panel parses the first 10 characters as a Crockford Base32 number, and it converts that 48-bit value into both a raw millisecond count and a human-readable UTC ISO timestamp. The decode path mirrors the encode path used during generation, which keeps round-trips lossless for any canonical input.
| Decode input | Decode output | What it tells you |
|---|---|---|
| Canonical 26-character ULID (case-insensitive, alphabet-normalized) | Millisecond integer | The exact 48-bit Unix millisecond embedded in the time field |
| Same input | UTC ISO timestamp | The same instant in ISO 8601 form, displayed in UTC |
| String beginning with 8 or higher, or longer than 26 characters | Rejected | More than 128 bits: not a canonical ULID |
| String containing I, L, O, or U | Rejected | Character outside the canonical Crockford Base32 alphabet |
The panel does not upload the identifier anywhere, so it is safe to paste internal record IDs, trace IDs, or event IDs for inspection. A displayed ISO time is UTC; local calendar presentation can differ from the displayed UTC by time zone, but the underlying millisecond value does not change. The page reads Date.now only inside the Generate click handler, never during server rendering, which keeps the initial page deterministic on every load.
Store bulk ULIDs without breaking the sort order
The lexical time ordering that makes ULIDs useful only holds if the database preserves the string exactly as written. A case-preserving column exactly 26 characters wide, paired with an ordinary bytewise or ASCII-compatible collation, is the minimum storage contract. Case-folding, locale collation, truncation, padding, or storage in an undersized column can all break the intended order, so always test the exact schema and collation before relying on a ULID column for time-ordered indexing.
Uniqueness is a separate concern. The 80-bit random field makes accidental collisions extremely unlikely, but the tool itself cannot coordinate with other tabs, devices, services, or previously generated values. A durable store should enforce a unique constraint on the ULID column and handle a conflict atomically rather than relying on a check-then-insert race as the final uniqueness control. Once both the storage width and the constraint are correct, a batch of identifiers that you generated together will sort by creation order, and any later identifier you decode will line up against that order by its millisecond timestamp.
Limits of bulk ULID timestamp decoding
The Decode panel accepts one canonical string per paste, so a strict bulk decoder for thousands of existing identifiers is not part of this tool. For very large batches of pre-existing ULIDs, plan to repeat the paste or run a small script that mirrors the same Crockford Base32 decoding rule. The Generate panel does produce a true bulk batch of 1 to 100 identifiers per click, which is the easiest way to get a known shared timestamp across many rows.
Several other limits are worth keeping in mind. The visible timestamp is part of the design, not a bug, so treat ULIDs as metadata about creation time rather than authenticated truth, and avoid logging them when they are linked to personal or confidential records. An identifier is not a password, API key, bearer token, authorization rule, or encryption key, and the embedded timestamp does not protect a record from enumeration if access control is missing. Use a dedicated credential system for secrets and a secret manager for storage. For readers who need to convert the decoded millisecond into a JavaScript Date object, the Convert Unix Timestamp to Date in JavaScript guide covers that step in detail.
Related reading: How to Convert Unix Timestamp to Date in Python.