HackerReposA FIELD GUIDE TO OPEN SOURCE
ProSponsor the shelf♧ 0
← Back to top all-time
ulid
spec
OPEN SOURCE2018–2026
ulid / spec

spec

The canonical spec for ulid

63Distinct people
70Mentions
1Explicit recommenders
10.9KGitHub stars
REPOSITORY AT A GLANCE

What to know before exploring

What it is

The canonical spec for ulid

GitHub

Where it is used

No documented product use was verified in the last source check.

What people are saying

I've been using ULIDs [0] in prod for many years now, and I love them. I just use string encoding, though if I really wanted to squeeze out every last MB, I could do some…

kortex used a recommendation phrase. One indexed comment matches. This is a phrase match, not a verdict.

No classified warning is attached to this page. Read the discussions before treating silence as approval.

Also in these discussions

Mentioned in the same Hacker News threads. That is not proof they are substitutes.

This week on Hacker News

Nov 7–Nov 14 UTC · Direct repository links and separately labeled related project stories. Indexed through 11/14/2026, 1:49:46 PM UTC.

No direct repository links were found in the indexed HN discussions over the past seven days. Older discussions are available below.

Mentions over time

HN mentions by year70 dated mentions

Observed mentions: 2018-09-01 02:53:42 UTC to 2026-10-24 03:00:58 UTC.

0510152018-01-01 00:00:00 UTC to 2019-01-01 00:00:00 UTC (exclusive): 2 mentions; 2 submissions, 0 comments20182019-01-01 00:00:00 UTC to 2020-01-01 00:00:00 UTC (exclusive): 5 mentions; 1 submissions, 4 comments2020-01-01 00:00:00 UTC to 2021-01-01 00:00:00 UTC (exclusive): 8 mentions; 1 submissions, 7 comments2021-01-01 00:00:00 UTC to 2022-01-01 00:00:00 UTC (exclusive): 14 mentions; 2 submissions, 12 comments2022-01-01 00:00:00 UTC to 2023-01-01 00:00:00 UTC (exclusive): 12 mentions; 2 submissions, 10 comments20222023-01-01 00:00:00 UTC to 2024-01-01 00:00:00 UTC (exclusive): 16 mentions; 1 submissions, 15 comments2024-01-01 00:00:00 UTC to 2025-01-01 00:00:00 UTC (exclusive): 6 mentions; 2 submissions, 4 comments2025-01-01 00:00:00 UTC to 2026-01-01 00:00:00 UTC (exclusive): 4 mentions; 0 submissions, 4 comments2026-01-01 00:00:00 UTC to 2027-01-01 00:00:00 UTC (exclusive): 3 mentions; 1 submissions, 2 comments. Incomplete calendar period.2026*

* An incomplete calendar period is marked with an outlined bar. Its count is not directly comparable with a full period.

Counts come from the repository’s dated source evidence. Index cutoff: 2026-11-02 08:16:59 UTC.

View mention counts and date ranges
UTC ranges include their start and exclude their end. Select a period to filter the source evidence.
PeriodStart (UTC)End (exclusive, UTC)MentionsSubmissionsComments
20182018-01-01 00:00:00 UTC2019-01-01 00:00:00 UTC220
20192019-01-01 00:00:00 UTC2020-01-01 00:00:00 UTC514
20202020-01-01 00:00:00 UTC2021-01-01 00:00:00 UTC817
20212021-01-01 00:00:00 UTC2022-01-01 00:00:00 UTC14212
20222022-01-01 00:00:00 UTC2023-01-01 00:00:00 UTC12210
20232023-01-01 00:00:00 UTC2024-01-01 00:00:00 UTC16115
20242024-01-01 00:00:00 UTC2025-01-01 00:00:00 UTC624
20252025-01-01 00:00:00 UTC2026-01-01 00:00:00 UTC404
2026 (partial)2026-01-01 00:00:00 UTC2027-01-01 00:00:00 UTC312

From the project README

An excerpt from the repository, fetched Nov 13, 2026.

Read the project’s introduction
Universally Unique Lexicographically Sortable Identifier UUID can be suboptimal for many use-cases because: It isn't the most character efficient way of encoding 128 bits of randomness UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures UUID v4 provides no other information than randomness which can cause fragmentation in many data structures Instead, herein is proposed ULID: ulid() // 01ARZ3NDEKTSV4RRFFQ69G5FAV 128-bit compatibility with UUID 1.21e+24 unique ULIDs per millisecond Lexicographically sortable! Canonically encoded as a 26 character string, as opposed to the 36 character UUID Uses Crockford's base32 for better efficiency and readability (5 bits per character) Case insensitive No special characters (URL safe) Monotonic sort order (correctly detects and handles the same millisecond) Implementations in other languages From ourselves and the community! Language Author Binary Implementation C++ suyash ✓ C# mcb2001 Clojure theikkila Objective-C ricardopereira Crystal SuperPaintman Dart isoos ✓ Dart GuepardoApps Delphi matinusso D enckse D (dub) extrawurst Erlang savonarola Elixir Homepolish ✓ Elixir merongivian Elixir omisego ✓ Elixir (Ecto) dcuddeback ✓ F# lucasschejtman Factor Alexander Ilin ✓ Go oklog ✓ Haskell steven777400 ✓ Java huxi ✓ Java azam Java Lewiscowles1986 JavaScript ulid JavaScript aarondcohen ✓ Julia ararslan Kotlin GuepardoApps Lua Tieske .NET RobThree ✓ .NET fvilers Nim adelq OCaml stripedpajamas Perl 5 bk ✓ PHP Lewiscowles1986 PHP robinvdvleuten PowerShell PetterBomban Python mdipierro Python ahawker ✓ Python mdomke ✓ R hrbrmstr Ruby rafaelsales Rust mmacedoeu ✓ Rust dylanhart ✓ Scala petitviolet SQL-Microsoft rmalayter ✓ Swift simonwhitehouse Swift yaslab ✓ Tcl dbohdan Specification Below is the current specification of ULID as implemented in ulid/javascript. Note: the binary format has not been implemented in JavaScript as of yet. 01AN4Z07BY 79KA1307SR9X4MV3 |----------| |----------------| Timestamp Randomness 48bits 80bits Components Timestamp 48 bit integer UNIX-time in milliseconds Won't run out of space 'til the year 10889 AD. Randomness 80 bits Cryptographically secure source of randomness, if possible Sorting The left-most character must be sorted first, and the right-most character sorted last (lexical order). The default ASCII character set must be used. Within the same millisecond, sort order is not guaranteed Canonical String Representation ttttttttttrrrrrrrrrrrrrrrr where t is Timestamp (10 characters) r is Randomness (16 characters) Encoding Crockford's Base32 is used as shown. This alphabet excludes the letters I, L, O, and U to avoid confusion and abuse. 0123456789ABCDEFGHJKMNPQRSTVWXYZ Monotonicity When generating a ULID within the same millisecond, we can provide some guarantees regarding sort order. Namely, if the same millisecond is detected, the random component is incremented by 1 bit in the least significant bit position (with carrying). For example: import { monotonicFactory } from 'ulid' const ulid = monotonicFactory() // Assume that these calls occur within the same millisecond ulid() // 01BX5ZZKBKACTAV9WEVGEMMVRZ ulid() // 01BX5ZZKBKACTAV9WEVGEMMVS0 If, in the extremely unlikely event that, you manage to generate more than 280 ULIDs within the same millisecond, or cause the random component to overflow with less, the generation will fail. import { monotonicFactory } from 'ulid' const ulid = monotonicFactory() // Assume that these calls occur within the same millisecond ulid() // 01BX5ZZKBKACTAV9WEVGEMMVRY ulid() // 01BX5ZZKBKACTAV9WEVGEMMVRZ ulid() // 01BX5ZZKBKACTAV9WEVGEMMVS0 ulid() // 01BX5ZZKBKACTAV9WEVGEMMVS1 ... ulid() // 01BX5ZZKBKZZZZZZZZZZZZZZZX ulid() // 01BX5ZZKBKZZZZZZZZZZZZZZZY ulid() // 01BX5ZZKBKZZZZZZZZZZZZZZZZ ulid() // throw new Error()! Overflow Errors when Parsing Base32 Strings Technically, a 26-character Base32 encoded string can contain 130 bits of information, whereas a ULID must only contain 128 bits. Therefore, the largest valid ULID encoded in Base32 is 7ZZZZZZZZZZZZZZZZZZZZZZZZZ, which corresponds to an epoch time of 281474976710655 or 2 ^ 48 - 1. Any attempt to decode or encode a ULID larger than this should be rejected by all implementations, to prevent overflow bugs. Binary Layout and Byte Order The components are encoded as 16 octets. Each component is encoded with the Most Significant Byte first (network byte order). 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 32_bit_uint_time_high | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 16_bit_uint_time_low | 16_bit_uint_random | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 32_bit_uint_random | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 32_bit_uint_random | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Prior Art Partly inspired by: http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html
Read the full README on GitHub

What Hacker News says

70 sources
If you really need monotonic, secure/unguessable IDs, there are options like CUID2 (github.com/paralleldrive/cuid2). Older designs like ULIDs (https://github.com/ulid/spec) and Snowflake IDs (https://en.wikipedia.org/wiki/Snowflake_ID) have similar properties to UUIDv7, and potentially some advantages w.r.t. length/encoding/runtime cost/etc. But UUIDv7 has one key advantage none of those do: you can store them in a UUID-typed column or struct field without any lossy conversion. All of the UUID versions share a common size and serialization format, so it doesn't actually matter if you generate v1/4/7/etc. Many language standard libraries can generate them interchangeably, and (for example) a Postgres table can store any of the versions in the same column type. So yes, to the author's point(s): they are not magic, monotonic, un-guessable ID
Read the rest
s and you shouldn't treat them as either "secure" or strictly ordered. That doesn't mean they aren't useful.
In UUIDv7: Timestamps aren’t time-ordered and other oddities
kortex·Dec 15, 2025Recommendation phrase
I've been using ULIDs [0] in prod for many years now, and I love them. I just use string encoding, though if I really wanted to squeeze out every last MB, I could do some conversion so it is stored as 16 bytes instead of 26 chars. In practice it's never mattered, and the simplicity of just string IDs everywhere is nice. Sometimes I have to talk to legacy systems, all my APIs have str IDs, and I encode int IDs as just decimal left padded with leading zeros up to 26 chars. Technically not a compliant ULID but practically speaking, if I see leading `00` I know it's not an actual ULID, since that would be before Nov-2004, and ULID was invented in 2017. The ORM automatically strips the zeros and the query just works. I'm just kind of over using sequential int IDs for anything bigger than hobby level stuff. Testing/fixturing/QA are just so much
Read the rest
easier when you do not have to care about whether an ID happens to already exist. [0] https://github.com/ulid/spec
In Avoid UUID Version 4 Primary Keys in Postgres
On Thursday I learned about ulid[0] which I think really neatly solves the problem of text representation for UUID v7. However, I also like the idea of prefixed ids, although I haven't used them in anger. Yesterday I built most of a Postgres extension, using the excellent pgrx[1] project, that build on ulid to add prefixes. With it you get something like this plid=# SELECT gen_plid('u'); gen_plid --------------------------- u_06DHRQH6SJT7N2WEQK4910R (1 row) The aim is for it to be the same size as a UUID in storage, but I haven't quite gotten there yet. I haven't pushed it to GitHub yet, but it's fairly done at this point. 0: https://github.com/ulid/spec 1: https://github.com/pgcentralfoundation/pgrx
In Ask HN: What Are You Working On? (December 2025)
My org has been using ULID[0] extensively for a few years, and generally we've been quite happy with it. After initially dabbing with a few implementations, I reimplemented the spec in Kotlin, and this has been working out quite well for us. We will open-source our implementation in the following weeks. ULID does specifically require generated IDs to be monotonically increasing as opposed to what the RFC for UUIDv7 states, which is a big deal IMHO. [0]: https://github.com/ulid/spec
In Postgres UUIDv7 and per-back end monotonicity
The closest that comes to minds is ULID[0]. It is short (26 character base32), 128 bit and lexicographically sortable. I think the reason there's no other popular standard is you give up something. 128 bit gives a pretty low risk of collisions in almost all uses, but as you go smaller you start having to consider the specific scenario and impact, etc, which doesn't work well for a standard. You could use another encoding (eg base64 or base85) to get it shorter, but you start sacrificing other things (case sensitivity, url-safeness) - again, not great for a standard. [0] https://github.com/ulid/spec
In TIL: Versions of UUID and when to use them
UUIDv7 is a nice idea, and should probably be what people use by default instead of UUIDv4 for internal facing uses. For the curious: * UUIDv4 are 128 bits long, 122 bits of which are random, with 6 bits used for the version. Traditionally displayed as 32 hex characters with 4 dashes, so 36 alphanumeric characters, and compatible with anything that expects a UUID. * UUIDv7 are 128 bits long, 48 bits encode a unix timestamp with millisecond precision, 6 bits are for the version, and 74 bits are random. You're expected to display them the same as other UUIDs, and should be compatible with basically anything that expects a UUID. (Would be a very odd system that parses a UUID and throws an error because it doesn't recognise v7, but I guess it could happen, in theory?) * ULIDs (https://github.com/ulid/spec) are 128 bits long, 48 bits encode a
Read the rest
unix timestamp with millisecond precision, 80 bits are random. You're expected to display them in Crockford's base32, so 26 alphanumeric characters. Compatible with almost everything that expects a UUID (since they're the right length). Spec has some dumb quirks if followed literally but thankfully they mostly don't hurt things. * KSUIDs (https://github.com/segmentio/ksuid) are 160 bits long, 32 bits encode a timestamp with second precision and a custom epoch of May 13th, 2014, and 128 bits are random. You're expected to display them in base62, so 27 alphanumeric characters. Since they're a different length, they're not compatible with UUIDs. I quite like KSUIDs; I think base62 is a smart choice. And while the timestamp portion is a trickier question, KSUIDs use 32 bits which, with second precision (more than good enough), means they won't overflow for well over a century. Whereas UUIDv7s use 48 bits, so even with millisecond precision (not needed) they won't overflow for something like 8000 years. We can argue whether 100 years is future proof enough (I'd argue it is), but 8000 years is just silly. Nobody will ever generate a compliant UUIDv7 with any of the first several bits aren't 0. The only downside to KSUIDs is the length isn't UUID compatible (and arguably, that they don't devote 6 bits to a compliant UUID version). Still feels like there's room for improvement, but for now I think I'd always pick UUIDv7 over UUIDv4 unless there's an very specific reason not to. Which would be, mostly, if there's a concern over potentially leaking the time the UUID was generated. Although if you weren't worrying about leaking an integer sequence ID, you likely won't care here either.
In Goodbye integers, hello UUIDv7

Showing the 15 most recent of 70 sources. Enable JavaScript to search and browse every indexed mention.

MAKE YOURSELF AT HOME

A few shortcuts

/
Search the collection
?
Open this guide
Esc
Close dialogs and navigation

Your saved repositories and interests stay in this browser.