Comment by mittermayr

1 day ago

I fully agree. It makes no sense. Yet...

The only guesses I'm having is that we originally generated UUIDv4s on a user's phone before sending it to the database, and the UUID generated this morning that collided was created on an Ubuntu server.

I don't fully know how UUIDv4s are generated and what (if anything) about the machine it's being generated on is part of the algorithm, but that's really the only change I can think of, that it used to generated on-device by users, and for many months now, has moved to being generated on server.

You let users generate a UUID?

To be honest, the chance that you are doing something weird is probably higher than you experiencing a real UUID conflict.

How did your database 'flag' that conflict?

  • user-generated (as in: on the user's phone) was only at the very early stages of this product, and we've since moved to on-server. It's a cash-register type of app, where the same invoice must not be stored twice. So we used to generate a fresh invoice_id (uuidv4) on the user's device for each new invoice, and a double-send of that would automatically be flagged server-side (same id twice). This has since moved on to a server-only mechanism.

    The database flagged it simply by having a UNIQUE key on the invoice_id column. First entry was from 2025, second entry from today.

  • If it's UUIDv4 and you validate that the UUID is valid and not conflicting I don't really see the issue with user-generated UUIDs. Being able to generate unique keys in an uncoordinated manner is the main selling point of UUIDs

    Sure, it's something I'd flag in any design to spend two minutes to talk about potential security implications. But usually there aren't any

    • The whole point of UUIDv4 is that you don't need to check if it's conflicting and can just use them right away. This falls apart if you let untrusted sources of UUIDv4's enter your system IMO

    • Validation etc. every thing which should not be controlled by a user, will not be controlled by a user.

  • Likely a unique index... duplicate insert on a primary or 1:! foreign key. I am currently shimming out a process that will add a trackingid for a job service, and just had my method stub retorn Guid.Empty... second time I ran my local test it blew up on the duplicate key... then I switched it to null, then it blew up again... I neglected to exclude null from the unique index on the foreign key.

    In any case, it's easy enough to do. I mostly use UUDv7, COMB or NEWSEQUENTIALID ids myself though.

  • The smart way would be to check if the id is in use, and generate a new one... Repeat a few times if you're extremely unlucky, and bail out with an error if you have the absolute worst rng. It also works for locally generated ids as well.

If it was two on-device generated UUIDs I could see a collision happening. There have been instances of cheap end devices not properly seeding their random number generators, leading to colliding "random" values. And cases of libraries using cheap RNGs instead of a proper cryptographic RNG, making it even worse

But on a server that shouldn't happen, especially not in 2026 (in the past, seeding the rngs of VMs used to be a bit of an issue). Even if one UUID was badly generated, a truly random UUID statistically shouldn't collide with it. You'd need an issue in both generators

  • The library is using node:crypto, but with a phone target, that's likely shimmed with a JS implementation...

The UUIDv4 collision is statistically extremely unlikely. What is more likely is both systems used the same seed. This might be just a handful of bytes, increasing the chance of collision to one in billions or even millions.

  • The shim for node:crypto in the browser is likely a weaker implementation in JS than the node implementation... you can cheat and use the browser itself to get a UUIDv4...

        function uuid4() {
          var temp_url = URL.createObjectURL(new Blob());
          var uuid = temp_url.toString();
          URL.revokeObjectURL(temp_url);
          return uuid.split(/[:\/]/g).pop().toLowerCase(); // remove prefixes
       }

Better check what crypto.js is actually doing in your exact setup. Weak polyfills exist...