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 8–Nov 15 UTC · Direct repository links and separately labeled related project stories. Indexed through 11/15/2026, 12:45:45 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

16 sources
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
I'd love for Postgres to adopt ULID as a first class variant of the same basic 128bit wide binary optimized column type they use for UUIDs, but I don't expect they will, while its "popular" its not likely popular enough to have support for them to maintain it in the long run... Also the smart money ahead of time would have been for the ULID spec to sacrifice a few data bits to leave the version specifying sections of the bit field layout unused in the ULID binary spec (https://github.com/ulid/spec#binary-layout-and-byte-order) for the sake of future compatibility with "proper" UUIDs... Performing one quick bitfield modification on every row in PostgreSQL would have been less painful as in set bit, vs load parse, repack in order to re-computing the appropriate UUIDv7s (or UUIDv8s for some reason) since the primary key update transaction s
Read the rest
hould be roughly the same speed either way.
In 50 years later, is two-phase locking the best we can do?
I recently found ULID. I like its simplicity. It is sequential. It has a very low probability of collision. And it is of a more reasonable length. Because it encodes the time, theoretically you could use it to grab the CreatedDate of a record without a need for another field. https://github.com/ulid/spec
In You Don't Need UUID
While UUIDs are pretty common, they are by far not the only or superior solution. To be able to easily generate IDs on different nodes in a distributed system something host-specific, pure random, or a mixture of both is needed. To be avoid problems with random numbers on DB indexes some time-based part would be useful. There are other approaches like Ulid or TSID which are similar to UUID v7, but are represented in are more compact way. Example Ulid vs UUID: 000360TJXZDDMSKJSQGBQHA5YA fb87f306-b613-4948-be24-00609cf9ccc8 https://github.com/ulid/spec https://tsid.com/de
In Unexpected downsides of UUID keys in PostgreSQL
Does ULID solve both? https://github.com/ulid/spec 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)
In UUIDs vs. Serials for Keys
> nano id There are pros and cons to using random IDs as a PK. For RDBMS clustering on the PK (InnoDB), it's a terrible idea. If you're going to sort by the PK, it's usually a terrible idea (UUIDv1 isn't as bad since it includes the timestamp, but that assumes your access pattern is based on insertion time). There is ULID [0] if you'd like something that's sortable. You could also just have a secondary index. An advantage can be that it _can_ be a good way (again, this depends heavily on your access patterns) to do sharding. My other concern for nano id is twofold, both around their PRNG. They mention using Node's crypto.randomBytes(), but their source code instead references crypto.randomFill() [1]. Node's docs mention that having "surprising and negative performance implications for some applications" [2], related to libuv's thread pool
Read the rest
. See my later comment about libuv and containers. Also, Node's crypto.randomBytes() mentions that it "will not complete until there is sufficient entropy available." That sounds suspiciously like they're using `/dev/random` instead of `/dev/urandom`, which at least for this application of it, would be an odd decision. I did note that elsewhere in nano id, they're creating their own entropy pool, so it may not matter either way. With that out of the way: If the plan is only for self-hosting, then yeah, you don't really need to consider schema design that carefully. Databases are really good at their job. Also, honestly nearly none of this matters until you have significant scale. If you plan on starting a SaaS, there's a lot to consider. An incomplete list, in no particular order: * Foreign keys. They're very handy, but they can introduce performance problems with some access patterns. Consider indexing child table FKs (but not always - benchmark first). * DDL like ALTER TABLE. I suggest getting intimately familiar with Postgres' locks [3]. The good news is that instant ADD COLUMN with {DEFAULT, NOT NULL} is safer now. The bad news is that it does so by lazy-loading, so if your queries are doing silly things like SELECT *, you're still going to end up a ton of contention. * Connection pooling. You don't want to eat up RAM dealing with connections. PgBouncer [4] and Pgpool-II [5] are two that come to mind, but there are others as well. The latter also handles replication and load balancing which is nice. If you aren't using that, you'll need to handle replication and load balancing on your own. * Load balancing. HAProxy [6] is good for load balancing, but has its own huge set of footguns. Read their docs [7]. A few things that come to mind are: * Any kind of abstraction away from the CPU, like containers, may cause contention. Same with VMs (i.e. EC2), for that matter, since a noisy neighbor can drop the single-core turbo of Xeons A LOT. Look into CPU pinning if possible. * HAProxy really likes fast clocks over anything else, for x86. Xeons will beat Epyc. ARM can beat x86 if tuned correctly. * If you're using Kubernetes, look into Intel's CPU Management [8], which is also now native in K8s v1.26 [9]. * Overall for containers, learn about cgroups. Specifically, how they (both v1 and v2) expose the `/proc` filesystem to applications. Then at how your application is detecting that for any kind of multiprocessing. Hint: Node [10] uses libuv, which is calling `/proc/cpuinfo` [11]. * If you have access to the disk (e.g. you're running bare metal or VMs with this capability), think carefully about the filesystem you use and its block size (and record size, if you use ZFS). Good luck! [0]: https://github.com/ulid/spec [1]: https://github.com/ai/nanoid/blob/main/async/index.js#L5 [2]: https://github.com/nodejs/node/blob/main/doc/api/crypto.md#c... [3]: https://www.postgresql.org/docs/current/explicit-locking.htm... [4]: https://www.pgbouncer.org/ [5]: https://www.pgpool.net/mediawiki/index.php/Main_Page [6]: https://www.haproxy.org/ [7]: https://cbonte.github.io/haproxy-dconv/2.4/configuration.htm... [8]: https://networkbuilders.intel.com/solutionslibrary/cpu-pin-a... [9]: https://kubernetes.io/docs/tasks/administer-cluster/cpu-mana... [10]: https://github.com/nodejs/node/blob/main/src/node_os.cc#L100 [11]: https://github.com/libuv/libuv/blob/v1.x/src/unix/linux.c#L8...*
In Launch HN: Activepieces (YC S22) – Open-Source Zapier Alternative

Showing the 15 most recent of 16 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.