juliolinarez
ExCuid2 - Elixir implementation of CUID2 (Collision-Resistant Unique Identifiers)
ExCuid2 generates secure, collision-resistant unique identifiers designed for efficiency and horizontal scaling. They are an excellent choice for primary keys in distributed databases. Includes a type to work with Ecto
Features:
- Collision-Resistant: Uses multiple entropy sources to minimize the probability of collisions, even in high-concurrency systems.
- Secure: Starts with a random letter to prevent enumeration attacks and uses
:crypto.strong_rand_bytesfor cryptographically secure entropy. - Scalable: Includes a process fingerprint to ensure uniqueness across different nodes and application restarts.
- Efficient: Implemented with a stateful
Agentto manage an atomic counter quickly and safely. - Customizable: Allows generating IDs with a length between 24 and 32 characters.
- Supervisable: Can be added directly to your application’s supervision tree.
Thanks
Most Liked
garrison
This is not a comment on your implementation, but I had never heard of these before and had a look at the original repo and I have to say it gives off some weird vibes. It seems like their main argument is that UUIDv4 is bad if you use a weak source of entropy (yeah, obviously) and that CUID is better because it combines “multiple sources of entropy”.
There are a lot of weird claims about security and other things. The section on k-sortability is particularly bizarre (“cloud databases” store all their data in memory so it doesn’t matter? seriously?).
Anyway, :crypto.strong_rand_bytes() should have sufficient entropy to generate ids, or at least I hope it does! Is there any other functionality that CUID2 offers over the (well-standardized) UUIDv4/7?
Of course, if someone is already using these it’s still good to have a library for them, so definitely don’t take my comments as directed at you ![]()
garrison
I really don’t mean to be rude here, but I would appreciate it if you could reply in your own words rather than with an LLM.
This entire reply is AI slop. Almost every part of it is completely wrong, and there’s no way for me to know whether it’s because you’re mistaken or because GlazeGPT is hallucinating nonsense.
I had a look at your code because I was curious and it seems to me to be AI generated as well. The implementation is not good. :crypto.strong_rand_bytes/1 is cryptographically secure and provides sufficient entropy.
You do not need to start an agent with a counter (serializing every single id generation via message passing in the process and incurring massive overhead) and then sha256 that with the actual random bytes. That does not add entropy. It’s completely absurd.
You are choosing the first letter with Enum.random(). That function is not crypto-safe, all you’re doing is decreasing the entropy of your ids.
Under no circumstances should anyone ever use this ridiculous id format for a new project, but if you have a legacy project you need to support then you can just generate random bytes with :crypto.strong_rand_bytes/1 and then base36-encode them directly. You do not need to implement any of this additional “entropy” nonsense.
I recommend you update your library to do exactly that.
garrison
Instead of using the original algorithm, you can just generate bytes with :crypto.strong_rand_bytes(n) and then base36-encode them (you already have code for this I think). That would produce a valid id with sufficient entropy.
I don’t know what the purpose of the letter thing is, but I guess it’s part of the spec so fair enough. The problem is that you’re generating the first letter with a weak source (Enum.random() with the defaults), so it’s not crypto-safe like the rest of the id. Maybe this matters in practice, maybe not, but it’s not ideal. For the record the original implementation also just uses Math.random() to do this (you know, the function they themselves are claiming is unsafe), which I find quite incredible, but whatever.
I believe you can seed using :crypto.rand_seed() to make it crypto-safe, but that will of course clobber the default random state for that process:
:crypto.rand_seed()
letter = String.graphemes("abcd...") |> Enum.random()
You can also create a state with :crypto.rand_seed_s/0 and then pass it to :rand.uniform_s/2 to do the same without clobbering the default PRNG for other purposes. I’m not sure of the tradeoffs between that and using :crypto.strong_rand_bytes/1, but perhaps somebody else could comment on that.
mpraski
Thank you for working on this library!
I’ve noticed you’re serializing access to the integer counter with an Agent - wouldn’t that become a performance issue in a high concurrency setting? Admittedly I didn’t read the CUID2 spec yet, but it doesn’t seem this ID was designed for distributed systems like the mentioned alternatives.
As for URL-friendliness I agree. I’ve come to use a “proxy” Ecto type that encodes the UUID using base62 and adds a prefix a’la Stripe identifiers. This results in a contiguous string (Dan Schultzer: Prefixed base62 UUIDv7 Object IDs with Ecto).
garrison
It’s fine to use them as DOM ids, but apparently it means you have to escape CSS selectors because they can’t start with numbers. I can see how that would be annoying, but in practice you rarely scope CSS to ids. Maybe for a weird case where you are using querySelector() with an id selector. I’ve never run into this before, personally.
It certainly would, but more importantly it’s completely pointless and, if anything, weakens the entropy of the ids. The purpose of the counter in the original implementation was to prevent sub-ms collisions from the clock source because they only had access to a millisecond timestamp in JS. This does not apply here, first of all because we have microsecond timestamps, but also because again :crypto.strong_rand_bytes() already has sufficient entropy.
Yes, it’s important to understand that the underlying format and the format you present can be entirely decoupled. A UUIDv4 is literally just 16 random bytes save for 6 bits which are set in a particular place to tell you “this is a UUIDv4”. For the other UUIDv* formats, a couple of those bits are different. You can encode the UUID however you want!
I have personally grown somewhat fond of base16 with the hyphens stripped. Base32/64 are really not meaningfully shorter from a UI perspective IMO. Subjectively, I mean.







