UUIDs, ULIDs, and Random Tokens: Choosing Identifiers That Don't Leak
Identifiers are one of those decisions that seem trivial until they are not. Pick the wrong kind and you leak how many customers you have, hand attackers guessable object references, or create a hot spot in your database index. Here is how to reason about the common options without overthinking it.
The problem with sequential IDs
Auto-incrementing integers (1, 2, 3, …) are simple and index beautifully, but they expose two things you often did not mean to share:
- Volume. If an order is
/orders/48213, a competitor knows roughly how many orders you have processed — and can watch the number climb over time. - Guessability. If access control is weak,
/orders/48214is a trivial guess. This is the classic insecure direct object reference (IDOR): the ID itself becomes an attack surface.
Sequential IDs are fine for internal primary keys behind solid authorization. They are risky the moment they appear in a URL, an email, or an API response.
UUID v4: random and opaque
A version-4 UUID is 122 bits of randomness formatted as xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx. It reveals nothing about volume or order and is effectively impossible to guess. Generate a batch with the UUID Generator and you will see the 4 in the third group (the version) and the 8/9/a/b variant nibble in the fourth — the structural markers that make a UUID a UUID.
The trade-off: pure-random UUIDs scatter across a database index, which can hurt insert performance and locality at scale because each new row lands in a random spot in the B-tree.
Why v7 and ULID exist
Newer identifiers try to keep the opacity of random UUIDs while restoring the index-friendliness of sequential ones. UUID v7 and ULID both put a millisecond timestamp in the high bits followed by randomness. The result sorts roughly by creation time — so new rows append near each other in the index — while the random tail keeps them unguessable and collision-resistant. The cost: the leading timestamp reveals when a record was created, which is usually fine but occasionally sensitive.
Rough rule of thumb:
- UUID v4 — you want maximum opacity and do not care about index locality.
- UUID v7 / ULID — you want opacity and time-ordered inserts (most high-write systems).
- Sequential int — internal keys only, never exposed, always behind authorization.
Tokens are a different job
An identifier names a thing; a token grants access to it. Do not confuse them. A password-reset link or an API key must be unguessable, which means it needs real cryptographic randomness and enough length — not a UUID repurposed as a secret. Generate those with the Password Generator, which uses crypto.getRandomValues, and treat their strength the way the Password Strength checker measures it: entropy in bits, not visual complexity. When a token must reach a teammate, hand it over with Secure Paste rather than pasting it into chat.
The short version
Expose random IDs (UUID v4, or v7/ULID when you also want time ordering), keep sequential integers internal and behind authorization, and never mistake an identifier for a secret — secrets need real randomness and real length. Spin up a few IDs in the UUID Generator to see the structure, and reach for the Password Generator the moment "ID" turns into "credential."
Sources
- RFC 9562 (UUID) — https://www.rfc-editor.org/rfc/rfc9562
- This article expands on original editorial guidance from Online Dev Tools.