Goodbye integers, hello UUIDv7
buildkite.com
buildkite.com
Having any information, specifically time information, leaking from your systems may or may not have unanticipated security or business implications. (e.g. knowing when session tokens or accounts are created).
I parsed the EV chargers APIs where I live (using Frida in Android) and one of the fields returned the daily revenue and profit.
I've seen a project for a trading firm that inferred all kinds of traffic and revenue numbers for companies before their quarterly earnings were made public. It wasn't perfect, but knowing with a certain confidence level whether the numbers were going to be better or worse than estimate was profitable for them.
And the indexing argument isn't really compelling, is it? You lose very little by sticking to fully random UUIDs.
Pizza itself might be a bit of a joke but looking for non-operational behavioral changes is absolutely real. The Cuban missile crisis was started in part because soviets played soccer and Cubans played baseball and the presence of soccer fields helped confirm soviet presence (in enough numbers to bother making rec centers). A more advanced version might be the public Strava data leaking US base layouts and locations or Strava helping the Ukrainians kill a Russian submarine commander.
Edit to add: you could also just figure out where your target orders pizza from and pay one of the dudes working there to tell you when there’s a spike in deliveries to your target.
It's a Henri Kissinger's quote, but it's not accurate: Cubans do in fact play football. Also this quote wasn't from the 1962 Cuban missile crisis, but to another event in 1970. That being said, it is true that the US intelligence got warned by the construction of football fields (or maybe even more so by the lack of baseball grounds).
https://www.cracked.com/article_31335_that-time-soccer-field...
iswydt
128 bits -> 128 bits
Alternatively, just say it's a random string ID and not an UUID.
[1] https://en.wikipedia.org/wiki/Format-preserving_encryption
If there's any way for the client to influence the input, it may be prone to DoS attacks: By my calculations, with a million random attempts, you would expect to find a cycle of length at least 435, which is over 13x the average. (Mind you, multiplying the number of attempts by 10 only adds about 72.5 to the expected cycle length, and probably no one has the patience to try more than 100 billion or so attempts.)
In any case, as I mentioned in a sibling comment, with 3 AES encryptions one can construct a 122-bit balanced Feistel cipher with a constant amount of work.
Breaking the 122 bits into two 61-bit halves and using AES as the round function for a Feistel cipher gives you a constant 3-encryption latency instead of the expected 64-encryption average latency of the cycle-walking format preserving encryption.
Alternatively, use AES in VIL mode ( https://cseweb.ucsd.edu/~mihir/papers/lpe.pdf ).
I’m afraid you won’t be able to ever rotate that key, would you? Since it’s result is externally used as an identifier, you would have to rotate the external identifiers, too.
You could rotate encryption keys whenever you want for new external id calculation, so that older external ids won't change (as they are external, they need to stay immutable).
Why not just hash it with pretty much any hash function?
If you didn’t care about index clustering then just use UUIDv4
If AES-128 is an acceptable external UUID (and likely an acceptable internal one), then you might as well just stick with a faster RNG.
The whole point here would be that you can expose something opaque externally but benefit from well behaved index keys internally.
Internal sorts and index on write with ULID/UUIDv7 which reveal potentially sensitive timestamp information, and so when that’s not appropriate a separate column can be used for…
External opaque identifiers which are UUIDv4, and if indexing speeds become an issue, can be switched to deferred indexing…
It’s a good balance and everything I’ve ever used supports this (I only needed a pretty strong representation wrapper for ULIDs since PG doesn’t validate UUID bit field structures so i can just miss-use UUID columns)
It looks like Snowflake has the same information exposure issues that using UUIDv7 or ULID for a publicly visible identifier.
Purely random identifiers are the bane of DB indices. The internal structure is sequential-ish and therefore indexes well.
Random identifiers spread work evenly between shards of something sharded by keyspace. They also don't get 'hotspotting' on more recent records - recent records frequently get more than their fair share of updates and changes, and database query planners have no knowledge of that.
Anything with a clustering index - MySQL (with InnoDB), SQL Server - will absolutely hate the page splits from a random PK.
Postgres also doesn’t like it that much, for a variety of reasons. Tuples are in a heap, but the PK is still a B+tree (ish), so it still suffers from splits. They also use half the default page size as MySQL, AND their MVCC implementation doesn’t lend itself to easy updates without rewriting the entire page.
Go run benchmarks with some decent scale (tens of millions of rows or more) on any DB you’d like between UUIDv4 and UUIDv7 as the PK, and see how it goes.
For tables this is not such a problem because of reasonably good locality (rows inserted close in time will end up close in the file too), but for indexes that's very difference. In Postgres this is particularly painful for writes, because of the write amplification.
None of this really breaks sharding (IDs are still unique and can be hashed), so no new hotspots. It can't fix the "many rows with the same ID" issue, but neither can any other ID.
For large distributed systems like Spanner you want to avoid a hotspot on a single node as that limits throughout.
However for a single-node system like PostgreSQL you want hot spots because they are cache friendly. (Locks aside)
Basically you want a hotspot that is as hot as a single node can manage, but no hotter. Even for things like Spanner you still want good cache hits (which is why they support table interleaving and other methods of clustering related data) but in general avoiding overloading single nodes is more important (as it hurts latency and efficiency but doesn't limit throughput like a hotspot would).
Might as well just use randomly generated external keys and have one table if you were doing that.
So, one key per all tokens.
Therefore there doesn't seem to be much downside in this specific case.
It feels like the right solution here is to pick between: use UUIDv7 and treat the account creation time as public information, or use an identifier scheme which doesn't contain the account creation time.
I mean it's certainly an improvement over telling everyone the millisecond the account was created, but if account creation times are to be considered non-public info, I would probably just not include any version of it in public facing user IDs. And if you do consider approximate account creation times to be public (such as HN, where anyone can see that my account was created July 1, 2015), then adding some fuzz to the timestamp seems to be a good way to avoid certain cryptographic issues.
https://app.example.org/file/1:abcdef12345
-> decrypt abcdef12345 with key "1" to yield UUIDv7 key of file (no matter what the latest key is)I think there is no good alternative to random external identifiers.
It is true that now your encryption key is now very long lived and effectively part of your public interface, but depending on your situation that could be an acceptable tradeoff, and there are quite a few pragmatic reasons why that might be true as has been described by other comments.
Edit: you can even do 64bit snowflakes internally to 128bit AES encrypted externally, doesn’t have to be 128-128 obvs
No need to encrypt, just store the external key in a table. Not that you're likely to change algorithms.
To that end, I think it's neat to be able to improve indexing on UUIDs, but it's not a security solution.
It may not be technically security, but e.g. knowing your competitor just added N products to their shop, might be a security issue for the business.
The end user should know no details about your internal key space.
We model our domain(s) using DDD, and often "The ID" really is best left a thing with meaning. Customer-id, Bank-account-number, invoice-number, email, etc. At least within the domain, it is. The business (and laws etc) already ensure there can only ever be one invoice with this number. Its terribly counterproductive to have two ID's for something. "Hey, can you have a look at invoice 20230233, because it seems the VAT was applied wrong. Hmm, do you have the UUID for that invoice and DM me that? You know, the long one with the hyphens".
I guess there isn't a one-size-fits all solution and that "it depends" very much on what e.g. "public" means.
It hardens, completes and complements other measures.
Examples of every day security using obscurity: every password and encryption key
EDIT: Thanks for the replies.
Ignore above!
Obscurity is the low bit of security. But when it’s convenient, it still helps.
Security by obscurity would be hiding your house key under a doormat for your friend to find - depending on the culture you live in you may be more or less safe but it is not security (just like hosting your ssh server on port 9384 will repel 99% of attackers but is not a security measure).
They are completely dumb. I haven't kept record, but I have the feeling that some IPs in my fail2ban list are practically in there for month or even years now.
I assume they are just sweeping the whole IPv4 range? No state, no cache. Either they successfully attack a host or they go to the next IP. Repeat 2^32 times, start again.
I'm not sure where I wanted to go with this comment. Is it _that cheap_ to constantly sweep the IPv4 range or is it _that profitable_ to do it once you have a successful attack?
You're getting that wrong: Everything else being equal, the more obscure system will always be the safer one. It's just that obscurity can easily be lost, so your system should, if in any way possible, still be secure even if fully known. In the end, however, no system is 100% secure, but more obscurity will make it harder to find the inevitably existing issues.
For a personal anecdote, I used to work in a small webshop and our software was horrible, to the point where minimal effort would have been able to compromise our servers, which were running software roughly as old as I was at the time (I want to note that I worked on improving the situation while I was there). Still, the only time we had a problem was when we took over a Joomla-hosted site, as we were small enough to not get any individual attention and your off-the-shelf WordPress or Joomla-scripts did not work on our home-brewed software.
In the end, I fully agree that security by obscurity is a weak concept and the usual wisdom of not relying on it is completely correct. Still, it's important to acknowledge that obscurity can and does help security and bring actual reasons on why you should not rely on it. Just saying "it's obviously bad" leads to an easily refuted argument and will not convince some developers, leading to worse software overall.
Not doing that is how small companies seem to get away with terrible security holes for a long time, until suddenly they don't. I've seen too many cases of companies in a position where they built a small, insecure service that's now getting shared more widely than envisioned, who don't want to spend the money to make it right, because no one has compromised it yet (that they know of), and what are the chances of someone stumbling across it - where even pointing out that it's an attack vector can earn you trouble.
This was used in the war to estimate the number of German tanks based on the sequential IDs
https://en.wikipedia.org/wiki/German_tank_problem
So just for business intelligence you don't want to leak your IDs.
Or from a list of most recently added customers/users, you can figure out the rate of signups.
Revealing timestamps is bad because it can reveal way too much information about the health of your business that you prefer to keep private, if a sequential list of ID's ever gets exposed (which is hard to prevent).
They're not monotonic.
My second job was for a company that provided internet enabled phone conferencing solutions (this was years before VoIP became widespread).
The customer ids were sequential. Couple that with an outright idiotic security flaw (the login process set the customer ID in a cookie and the app trusted it on ever subsequent request. Just the ID. Nothing else), I was able to iterate over all the customer ids and hand over a complete list of users to my boss to illustrate the problem, starting with a list of the accounts of the complete upper management.
They could have been used to spin up huge numbers of 30-person long distance conference calls at high costs (this company was building out nodes with 20,000 line pstn switches before they had customers... it was crazy, and they failed but would've failed far faster if that had been abused and they were on the hook for costs from their carriers)
Trusting that cookie was still stupid, but had it been a long random key it'd at least been a bit harder to discover and exploit (their next attempt was to base64 encode it and I had to explain why that didn't help; they then finally blowfish encrypted it, but without any time component, so still subject to replay attacks... I jumped at the first opportunity I got to get out of there)
To me, it reeks of solving imaginary problems while causing new ones.
That's about the only problem I can discern.
I don’t remember the details, so maybe it was something else and not photoshop/adobe.
If you don’t let an attacker iterate your data, all they can tell is when the ID was created.
The ordering means that you can reconstruct the sequence if you have enough of them, though.
The tank problem doesn't fit when the incrementing value is time since epoch. Integers yeah, UUIDs, KSUIDS, or any other semi-ordered thing to make your database indexes less fragmented I haven't seen a real leak issue with those.
We do a lot of computer vision and in his project, each processed object is assigned a UUID and he wanted to save images to files for each one.
So we took some time to go over various timestamp formats to be embedded into the filename to make the files sort chronologically. UUIDv7 is just spot-on solving our problem. In this use case, there are no real security considerations.
Obscurity can be helpful as part of defence in depth, to reduce the impact when someone does something stupid, or to make it more difficult to extract information that might be helpful as a means to attack the system from another angle.
If you're already thinking about the implications, you can likely ensure people doesn't jump to the conclusion that the IDs can be trusted just because they look complex.
They are compact, don't leak information, and make a good case why k-sortable IDs are unnecessary, or even harmful for performance.
I'm using sequential integers and created_at/updated_at timestamps for internal use, and Cuid2 IDs externally.
https://github.com/paralleldrive/cuid2#the-contenders
Some of the arguments mentioned are explained elsewhere in the README, others are assumed.
One argument standing out for me is the lack of collision-resistance for UUIDv4 which is surprising for me and I didn't spot any sources for that argument.
Another argument is the entropy source where they go about that Math.random is not reliable as a single entropy source but glimpsing at the source code, they sprinkle the CUID with Math.random data.
I am no expert in ID security, so I am not qualified to speak about the validity of their arguments, only that there's insufficient information to validate without prior knowledge about the problem domain.
Collision of UUIDV4 (which are 122 bits of entropy) are unlikely enough that it should fit most definitions of the word "impossible".
The argument listed in this library README feels like total bullshit to me, I'd avoid using it for this reason alone.
1. Collision resistance / "weak" PRGNs used to generate UUIDv4. Firstly, these are properties of the implementation, not the spec. Secondly, the source for calling the browser `Crypto.getRandomValues()` insecure is an issue that has been fixed back in 2016. I would not trust the developers of this implementation to do a better job than current browsers.
2. "Not URL or name friendly": Fair, but not very strong argument.
3. "Horizontally scalable" and "offline-capable": No argument given for why UUIDv4 does not meet these requirements, apart from point 1 above.
4. "Too fast": No argument given for why having a slower algorithm to generate random ids is more secure. Both UUIDv4 and Cuid2 use a similar number of random bits (122-124). When using a secure PRNG, both are equally difficult to guess, the SHA3 hashing doesn't add anything. You don't have to try and guess the "input" of the Cuid2 - you can just try to guess the "output" and skip the SHA3 hashing. It would be impossible to actually guess a generated ID, but UUIDv4 is just as impossible. Also no argument given for why UUIDv7 is fine but UUIDv4 is not.
I've used UUIDv4 for genering unique IDs for over 10 years now. I have run into collisions, when I hand-rolled my own implementation for J2ME with major bugs many years ago - ended up with around 20 bits of entropy instead of 122. That's not a reason to not use UUIDv4, just a reason to not implement it yourself unless you really know what you're doing.
Found them because systemd-journald isn't very happy when Boot IDs repeat and (apparently, then) stops showing earlier boots once it hits a repeating boot ID. And I wanted to see an earlier boot. Then I started logging the Boot ID in a textfile myself and it took less than 10 reboots to have duplicate Boot IDs.
Long story short, some weeks earlier, I "optimized" the Kernel config for that system and some config flags that didn't sound like something I'd need. As it turns out, an ARCH_ZYNQ target apparently also needs ARCH_VEXPRESS set. Otherwise it works absolutely fine, but with a broken RNG that you will notice weeks later.
That was a valuable "don't take down a fence until you know the reason why it was put up" lesson. Don't unset kernel config flags until you know why they are set.
Aside from breaking RNGs, I've never experienced any UUID collisions either.
If the algorithm is too fast it means you can detect when some other part of the system is having a significant impact on how the key is returned. Eg checking a database to see if a user exists and returning their key versus getting null back and generating a new key. That difference can be used to determine if a user exists. You want your key gen process to be slow enough that it's a significant part of the process, which makes timing attacks hard.
ID generation should usually only happen when creating new assets, so it should be as fast as possible.
From the article. I'd like a lot more exposition on that, since it goes against some of what used to motivate UUID use in the first place. Sequential ordering across distributed nodes isn't a fun thing to do, and even if you navigate the coding, the network agreement makes it really slow.
Do they mean "sequential enough" but some locality of the node that generated it? And I guess that prng can sometimes have some performance bottlenecks, but compared to doing locks on a single incremented integer?
Yeah, I don't really get this, lots of usual "better" "faster" etc without actual numbers to back it up or detailed algorithmic discussions.
As you basically said, wake me up if it's good enough to get into the standard vetted libraries of UUID generation.
> Cuid2 has been audited by security experts and artificial intelligence, and is considered safe to use for use-cases like secret sharing links.
I'm getting some snake oil vibes from this... There absolutely shouldn't be anything like a random ID that is 'too fast' to compute. You might need a rate limit to stay within your collision bounds, but CPU usage is a poor way to do it.
And there is currently no publicly available "artificial intelligence" that would be useful in a security audit, unless you want to call fuzzers "AI".
> One reason for using sequential keys is to avoid id fragmentation, which can require a large amount of disk space for databases with billions of records.
Disk is cheap but not free at higher tiers. But more importantly, record fragmentation means more pages (unless you take the time to do a full table lock and rewrite it, and who’s doing that?) which means more index bloat. I assure you, that adds up once you’re into the billions of records level.
> the ids will be generated in a sequential order, causing the tree to become unbalanced, which will lead to frequent rebalancing.
Given the width of B+trees used in DBs, I doubt they generally need to go more than one or at most two levels up. I’ll take the ability to rapidly follow the leaf nodes and have a good shot at sequential reads in cache from prefetch, thanks.
IMO it's not the job of the identifier itself to prevent information leakage vulnerabilities though; if thee is sensitivity to this, the solution should be explicit, such as employing a secondary key derived from the UUID using a secure KDF or similar.
Here uuidv7 will just re-order that. So the content of the uuid in itself does not change.
If you have 74 bits of entropy, the birthday paradox says that after 2^37 keys you will hit a 50% risk of a key collision. Whether that is going to be a problem depends on the use case, and on the quality of your RNG.
So what if your microblogging platform's tweet uuid happens to collide with my cocktail recipe generator's ingredient uuid? The likelihood that any one device will ever be running our apps at the same time and trying to read data from one into the other is even smaller than the 1-in-380000000000000000000000000000000000000 (uuidv4) probability of having a collision in the first place.
Applications use uuids to avoid colliding with themselves. Uuids exist so that web-apps can create objects client-side without waiting for a database CREATE, applications can be built with multi-node dbms, operating systems can name hardware compenents.
/s
Persistent IDs are a security and information risk. If that's a concern, don't persist IDs.
With UUIDv7, you are reasonably sure that there won’t be collisions (check your use-case first), and can just generate them wherever on-demand (no locks required).
I’d argue batching IDs is actually more complicated than UUIDv7 for most use-cases.
I don't think this is really true? These are not serially incrementing, they just indicate the time it happened. If you have an ID that you know exists, having the ability to know _when_ it was created is very rarely meaningful.
What could present more of a risk is being able to predict a large part of IDs that will be created. Even then though, you shouldn't depend on your IDs for secrecy - best to ensure the IDs are never used as protection by themselves (ie treat them like they're just a simple autoincrementing number, even if they're not)
You would be correct if the ID were an integer being serially increased. I can sign up to your website today and get an ID X and then sign up again in a week and get ID Y, I can then calculate the number of new users you've had by performing Y-X.
If this ID is a timestamp then there's no such information I can get out of it from a small sample. I sign up today and get todays timestamp, then I sign up next week and get next weeks timestamp..?
There are certainly mitigations that can be made and not all things are equally valuable as they age. (Plus many public APIs include created/modified timestamps anyway. The information is often easy to discover even when not embedded in an ID.) I don't find it a strong reason to avoid timestamp-based IDs for the threat models of that many things beyond user accounts and other things susceptible for social engineering, but it is something to keep aware of.
I still think that graph databases are way better for this sort of thing.
(for folks who don't get it, mullets are a 1980s haircut (think MacGyver) with a short front but a long tail in the back. A funny description of them is "business in the front, party in the back")
Fortunately, there's no rule that says you should shard your keys using the sequential part up front.
One of the rules for generating randomness from environmental sources is to throw away the high bits and only use the low bits. Distributed databases should do the same if they want a good distribution.
The closest I’ve ever heard of is sharding based on a hash (e.g. CockroachDB can do this on request[1]) but most distributed databases with strong consistency (Spanner descendants in particular) default to “doing it wrong”.
[1]: https://www.cockroachlabs.com/docs/stable/hash-sharded-index...
We even sharded on these columns, because of this (our business case made it so that hardly ever did people need data over multiple months)
But we never encountered distribution issues. I don't think the locality issue will be solved, as postgres doesn't consider other columns when distributing data, only the primary key IIRC. I don't know why we never saw this, though.
(Or just reverse the bits, take the last n, etc)
In both cases I'm melding highly disjointed data into a single schema. There are no large consecutive sets of records.
If you're using UUIDs, there's probably a reason. And that reason invalidates the justifications for not using them.
Not really; think of how many architectural decisions are made purely based on imaginary scaling problems, or what the latest blog said.
I would wager that if you polled 100 backend devs, very few of them could correctly articulate the pros and cons of a randomized primary key.
This is especially useful when your underlying database stores data in large "chunks", such as LSM-trees you find with e.g. rocksdb.
If you're using a system which is built for distribution, random is great.
When you're leaning on a Postgres database which has powered your startup through scaling but expects right-leaning btree indexes, it's a bad time.
Rearchitecting to use a new data store is ideal, but often impractical as an immediate step. UUIDv7 is a great increment walking that road via sharding etc.
Somebody posted an interesting article for the instagram ids, which do something similar. They use 41 bits for a time from a custom epoch followed two more groups of bits for a shard id and a sequential number. Each shard has an incrementing sequence for the sequential bit, which guarantees that things on a shard are sorted by time.
This UUIDv7 is slightly weaker than that but sorting things published in the same millisecond is mostly going to be very light work. The lack of a dedicated sharding group of bits is not that important as you could just take the n least significant bits at the end for that without too much effort. Those are random so you end up with nice consistent hashing. Having 48 instead of 41 bits for the time means we won't run out of time any time soon (nearly 9K years vs. 70 years).
Picking the N least significant bits only a single table has good distribution and sort qualities, no cross-table properties.
As a sibling comment says, you ideally want to shard on some other key to get "just enough" distribution that all your machines/disks have work to do, but you are still only hitting a limited number of hot sectors on each disk that can be effectively cached. But that requires active monitoring and rebalancing of your data as it grows. Totally random keys are a safe default that will scale with any kind of data distribution and access patterns.
v1: mac address + time + random
v4: completely random
v5: input + seed (consistent, derived from input)
v7: time + random (distributed sortable ids)
Depending on the implementation you still might have to worry about seeding issues. That's probably moot though since the UUID library would probably be compromised by something like that under the hood too.
Or if the same number of bits are used in a more structured manner, like uuidv1 which combines a 48 bit MAC address, 60 bit 100-nanosecond timestamp, and a 14 bit uniquifier with an effective resolution of 6.5 femtoseconds, you could have 300 quadrillion computers make 160 billion uuidv1s per second with guaranteed zero collisions until the year 3400 (plus an extra 500 or so years because the timestamp is referenced to 1542 for some silly reason), after which point any collisions that do happen will be completely irrelevant because they're guaranteed to be colliding with db entries created over 2000 years prior.
This would generate 2^127.8 UUIDs.
First, no, the collision would be expected in less than one year, approximately after exhausting square root of the available space (2^64 generated UUIDs): https://en.wikipedia.org/wiki/Birthday_problem
Second, no, the UUIDv4 has 122 random bits, not 128 as you thought: https://en.wikipedia.org/wiki/Universally_unique_identifier#...
In the real world, we do not spend 100% of our entire species's computing capacity generating uuids and doing nothing else. In the real world, we aren't burning through uuids at a rate of 2 billion per millisecond, and even if we were it wouldn't matter because the true denominator is the scope of the data system the uuid will be referenced in: if your hard drive partition and my webapp user entry happen to get the same uuid, we will never know, and if for some reason it does matter, then we can use uuidv1 or uuidv7 which guarantee no collisions for thousands of years by embedding a timestamp.
ksuid is similar to Twitter Snowflake, the main goal is distributed generation of collision-free sortable IDs. The UUIDv7 proposal is meant to address the same use case. You don’t need to worry about collisions as much here, as the timestamp is monotonically increasing, there is a 42-bit counter for every millisecond + the random 32 bits at the end. You’d have to be generating trillions of IDs per second to have the chance of a collision.
Generating sortable IDs in very high volume, in a distributed architecture, is a problem very specific to systems like social networks, metrics collection etc. You won’t need that for your average e-commerce app or machine parts database.
Unless you consider users being able to extract the generation time from the id to be an issue, of course.
Discord uses Twitter's Snowflake.
Which, despite the fact that it really shouldn't be, still seems to occur every so often. Even in situations where the ids are very much not random.
Honestly if I have to read one more article about a 'hacker' who 'leaked' some secret government piece ahead of time because they thought to increment the date in the url of some yearly report, I'm going to lose my mind.
The performance benefits of index friendly user IDs seem like they would apply even if all user info is secret and requires a token to access... The application still has to look up the user by ID after all?
If I imagine a basic authenticated "get information about me" style endpoint, that would take a user ID and an authentication token. Checking if the token is valid is faster if the user ID is index friendly. Getting the requested information is faster if the user ID is index friendly. Yet a user of the API still needs both the user ID and a token to access anything.
Ideally yeah.
In practice, it varies...
learned this last week that md5's are also 128 bits and so will fit perfectly in a uuid type in postgres, saving space in the physical table and indexes, and giving better index performance. So if youre storing md5 hashes, use UUID!
https://github.com/VADOSWARE/pg_idkit
There are a lot of options for UUID extensions (lots of great pure SQL ones!), but I wanted to get as many ID generation strategies in one place
Also note that native UUID v7 is slated to land in pg17:
The external key is base64 encoded for use in URLs which results in an 11 byte string.
This hides any information about the size of the data, the creation date of customer accounts (which would be sort of visible with UUIDv7) and prevents anyone from attempting to enumerate data by changing the integer in URLs.
I thought about using UUIDs as external keys but the only compelling use case seems to be the ability to generate keys from many decoupled sources that have to be merged later.
64 bit should be enough for most things https://youtu.be/gocwRvLhDf8?si=QBheJCG21bAAV0Z7
It's similar to UUIDv7 (it leaks the creation time), but it's not an issue for me.
So I am able to have a single 64 bit key, which can easily be formatted into a small string for user-facing urls.
[^1]: https://instagram-engineering.com/sharding-ids-at-instagram-...
Analyzing New Unique Identifier Formats (UUIDv6, UUIDv7, and UUIDv8) (2022) https://news.ycombinator.com/item?id=36438367
Half the point of these things is that they’re treated as opaque identifiers.
- if the ID is intended to be opaque then the vendor shouldn't document it as a UUID, as this changing to a different format would be a breaking change
- if the customer isn't going to process the subcomponents of the UUID then they should process it as an opaque string
- if the UUID library encounters a version number in a UUID it doesn't understand, it shouldn't reject the UUID but present it as an unstructured string.
After this blog post it seems likely that even Kite more customer will parse the IDs to extract time, since this has been documented.
All other versions, including the new v7, attach meaning to certain bits of the identifier. That cat has been out of the bag for a long time, so now everyone needs to maintain code to ensure that some rogue node doesn't spew back-dated identifiers belonging to the wrong department.
So then just a simple validation server side to ensure the data isn't malicious.
As Lazare points out in this thread they're basically the same thing, except with ULIDs you get those 6 extra bits of randomness back that UUIDs have to use for metadata.
ULID isn't an "official" standard like UUID. Having a real standard usually promotes interoperability and makes it easier to use. Additionally as others have pointed out you can already use UUIDv7 with some databases since it's just 16 opaque bytes and the database doesn't care what's actually in the UUID field.
Maybe if we were starting from scratch ULIDs would have been an option, but given where we were UUIDv7 was a much easier transition.
We've seen some amazing benefits, especially around improving the speed of batch inserts.
You set the node field to a broadcast MAC address, and use that as a namespace/prefix. This inches close to the boundary of the RFC, but is arguably compliant.
As an example, you may generate demo or “canary” data items that are UUIDv1s with a well known node field, which then lets you do distributed “isDemoData()” checks by just looking at the UUID.
I mean this is a similar concern to sequential IDs: many apps do not want to leak them, and in some cases it might cause issues, but in general it doesn't matter.
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 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.
Yep.
> A second is fine for that.
Not when you're doing O(1k-1M) operations per second, it isn't!
I could definitely be off. I work at a company that gets those levels of traffic but don’t deal with it directly.
Also, to all future historians of 2150, sorry about the mess, but yes we knew this was going to happen. Whatever it was.
It gets you precisely 100x further.
I wish UUIDv7 pulled the version/variant bits up front, though, just to make sure that the identifiers don't all start with null bytes.
https://www.ietf.org/archive/id/draft-peabody-dispatch-new-u...
"100 years should be enough" is what led us to a mountain of Y2K issues, because when would a two digit year ever be ambigious?
But I guess it's a psychological issue. Unless you're a megalomaniac, it's just natural to assume that your decisions won't matter much outside of your life and lifetime. And in that case, 100 years totally is enough because I probably won't live that long. And even more, in a lot of cases, it's also the correct assumption and the project won't live longer than a few years.
So, thinking about it, unless you are developing a novel standard or something that you want the world to adopt, 100 years probably IS fine. Unfortunately, KSUID wants to be a novel standard, so there's an issue.
> values generated are practically sequential
These statements aren’t strict enough to be relied on. Maybe you have engineered the hell out of your distributed clock scheme, and your IDs actually are completely monotonic, which is great. But you probably haven’t done that, which means conflicts will surely happen and you must handle them gracefully.
I mean that you can’t rely for correctness on time(X) < time(Y) when X happened before Y. It’s damn hard to keep two commodity server clocks within ±1 ms of each other even within a single LAN, and across production you’re more likely to see ±10 ms, or worse if your sysadmins don’t realize you intend to bet the farm on no clock skew.
ULID on the other hand does address this case by providing monotonicity within an epoch for a given producer. So would still need to treat each producer as a separate partition of the key space but you could order X > Y as long as both were produced by same producer (which effectively acts like a sequencer in this case).
EDIT: nvm, the UUIDv7 spec -allows- for arbitrary allocation of the remaining 62 bits which can be used as a counter: https://www.ietf.org/archive/id/draft-peabody-dispatch-new-u... All points for ULID can also apply to UUIDv7 depending on generation algorithm.
Maybe there are applications where the monotonicity matters, but in my experience reasoning by surrogate key is rather coarse grained and you manually scrutinize the boundaries, so unless your clocks are quite wrong, your worries are probably better placed elsewhere.
where ts between txn_start and txn_end
order by ts
and not even realize that what they’re seeing is incomplete and misleading. Clock skew is very common, and we shouldn’t sweep that under the rug to promote time ordering, because people want to believe this works the way they think.Out of curiosity, are you into hybrid logical clocks?
Yeah, though I’m more likely to go with a region ID and monotonic version number to compare-and-set and verify gapless data, where versions from different regions aren’t comparable. Actually I think earlier UUID RFCs talk about a “clock sequence” to distinguish timestamps from separate monotonic sources, but this paper doesn’t bring that up (or mention multiple clocks at all).
So any thing you can link, let me know.
For providing better query locality it probably doesn't matter significantly though which seems to be the main benefit here while preserving the other benefits UUIDs provide.
In other words: Sorting by millisecond-or-so is just as good as sorting by picosecond in most situations. The reason you have to deal with conflicts gracefully isn't particularly because timestamps can be imperfect.
None of our systems require perfect ordering of IDs generated across our distributed system. Most of the system was built with random UUIDv4 identifiers so no code assumes the ID ordering is significant.
However, in much of our system recent data is frequently accessed while old data is rarely accessed. In that world, just having the IDs *approximately* clustered in creation order has been a huge performance boost for many queries, and we've seen significant reduction in postgres Write Ahead Log rates, because writes to UID indexes happen in a smaller number of pages.
Thank you. I’m so tired of seeing the same groupthink on UUIDv4 trotted out - “it only matters if you have a clustered index, Postgres is immune!” The hell it is.
For another project, I've also used sortable 64-bit snowflake-like identifiers; they have the added benefit of being able to use 64-bit integer representation in code and database identifiers, even if you might want to externally represent them in base58 or similar encoding.
The original UUID types aren't as useful as they once were, so it'd be worth writing a new RFC and extending those original types.
I can therefore easily generate a new UUID in a trusted backend service which just accepts the command received from the untrusted client and then forwards the request for asynchronous processing while returning the UUID to the client. This is a typical architecture and the only change is that I can now create UUIDs which may have performance benefits, depending on the data storage technology of my read models.
If you need to create the UUIDs on the client side to support specific requirements such as offline-first, then I would indeed consider adding some reconciliation which replaces the IDs provided by the client-side by new ones generated by a trusted component as soon as synchronizing takes place.
It might be insignificant, but to me it makes UUID feel tainted, dirty. 11.1% of a UUID are dashes. 15.3% of a UUID are wasted bits if you count version and variant bits.
Anecdote: I worked for a company that used numeric primary ids internally and externally and increased the primary key by TWO to THREE for each new customer to make it appear to the outside world we had twice to three times the rate of customer growth.
Later I was excited about the power and expressiveness of SQL and its extensions. There is a ton of leverage and you can make it so that interfacing with it directly becomes much more useful.
However now I’m in a different phase. I see it as a durable data structure. I think in terms of “what does it provide to make the overall system better?”
The issues around indexing and uuids that is discussed in the article fits nicely into this line of thinking.
In web development, database access and performance often dominates and infects the whole system.
The easiest is probably to just base64 the binary representation of the 128 bit number, which results in a 128/6=22 character string, which is a bit smaller.
If glyph-length and not byte-length is more important you could go even smaller but I'm less sure if that's a good idea.
So really, what are you trying to optimize?
They're also often used as part of a URL parameter:
http://myservice/orders/<uuid> etc etc
If you want to store UUIDs as compactly as possible you'd use 16 bytes.
If you want to store them as text, mapping them to Unicode would be a terrible idea because: many characters are from scripts you've never heard of, many characters look identical (Α vs A), many characters are decomposed and it can change the encoding if they're decomposed[1], &c.
Or better yet, only decorate one after it has been clicked by the user, that way when it appears again elsewhere, it stands out. If you make each one pretty you'll have made all of them ugly when viewed together.
When you take that UUID and go start sniffing around internal systems you're going to copy the UTF-8 string representation.
Couldn't that be solved with incremented serial numbers, rather than leaking time data?
This solution attempts to solve the sort-ability issue of current uuids by moving the timestamp to the most significant bits.
I would assume that `serial` would solve this problem too.
[1]: https://github.com/segmentio/ksuid which has very similar use cases.
For bigger/public projects I'd like to be able to add a sequence, node, and data center id to the UUID too.
Unless you're manually storing them as strings... (Not ideal, but most dbs are pretty good at dealing with that too)
IE, take a 32 or 64 bit int that's the primary key, encrypt it, and then use that as the public ID in a web application, URL, API, ect.
a) UUID4, CreatedTime/UpdatedTime.
b) Bigint, CreatedTime/UpdatedTime.
c) UUID7 internal (which also includes time badly), UUID4 external/whatever short ID.
How exactly this helps if you need external ids (which you usually do today)? It doesn't even make it a short ID.
Even if there is a corner case, are we just saving a few bytes while adding more complication?
Clustered Index is a myth in PostgreSQL, not practical since you have to run a special program to reorder. So, a regular index might suffer but not really. Why? Because I am not ordering by the ID most of the time, I am ordering by "Created Date/Updated Date" or Name or whatever. Who cares about ordering IDs?
WAIT!!! But what about Next Tokens? ok, these are painful, but easily solved: Next can be (>=Created Date,>ID). Same result. Pagination, stays the same since it is sorted by Created Date.
The external Id is used instead of Bigint because you don't want your external users to query 1, then 2, then 3 (IDOR)... But the random part of the Uuid7 makes this impossible.
Uuid7 isn't a substitute for Created/Updated, but a substitute for the dual field Uuid4/Bigint.
GET /filter?a_id=X&b_id=Y&c_id=Z&d_id=w
But in practice we were using POST and passing the ids in the body payload. Why Because my old team said "the UUIDs are long, so we may reach the maximum URL length if we pass them as parameters". I didn't like it, and I still don't like it at all.Another thing is that you don’t necessarily need to encode uuids canonically. They are just u128’s. It’s relatively straightforward to find a url friendly string representation that is shorter.
Are we talking about shortening the whole URL or shortening specific UUIDs? If the latter then I imagine one would still need to keep track of the mapping UUID <-> shorten version, somewhere, right? If so, why not just add yet another field/column for an old good numeric integer that can be used for filtering? Would that work?
So a nonstandard but isomorphic shorter string representation.
Other than that; in IE6 times URL's had a limitation of 1K or 4K or something around that lenght IIRC, so unless you're using dozens of UUID's in a URL this hasn't been a problem for ages.
Note that you can also use GET with a body, it’s not spec compliant (a body is allowed but not supposed to have any meaning) but is used by products such as Elasticsearch. If you control both clients and servers that’s something you can safely do (and use an etag header for idempotency).
https://blog.devgenius.io/analyzing-new-unique-identifier-fo...
Hope it won’t bite me in the future.
If you need to sort by insert order, use an autoincrementing integer, if you need uniqueness, UUIDv4 is fine, if you need both use both.
Use timestamps when you need to record the time, just don't commit the sin of presuming that clock time will never run backwards, I assure you, it does.
I hold to the principle that relational data should be normal, and combining uniqueness with a timestamp doesn't do that. To do any of the calculations we use timestamps for, you have to strip off the entropy, this complicates pushing it down to the database level, where the libraries don't expect such conflation.
You're going to have a bad time writing something like a join across tables with a restricted range of time if your time is embedded in UUIDv7.
I maintain this is good advice: if you need index locality and insert order, use an autoincrement. If you need to record and work with time, use a timestamp. If you need global uniqueness, you can use any of the UUIDs, but v4 is the one that doesn't conflate uniqueness with unrelated properties, and should be preferred.
If you think your need data locality but not insert order, think long and hard about what you're doing, because odds are you're wrong. If it turns out you're right, and the OP might be in that situation, sure, go ahead and use UUIDv7.
Just, please, for the sake of your future self and everyone you work with, don't use a timestamp for insert order. Ever.
$ date -ud @$(( 256 ** 6 / 1000 ))
Tue Aug 2 05:31:50 AM UTC 10889Good question.
Won't random 128-bit numbers actually be superior to UUIDs in every way except predictability?
> As a result, retrieving the most recent data from a large dataset will require traversing a large number of database index pages, leading to a poor cache hit ratio (how many requests a cache is able to fill successfully, compared to how many requests it receives).