Shorter Than a UUID, Same Idea
A UUID is 36 characters and looks like machinery. For anything a person sees — a share link, a document key, a public object reference — a shorter URL-safe identifier does the same job in a third of the space.
The trade is explicit: fewer characters means fewer bits means a higher collision probability at a given volume. That is a decision to make deliberately rather than by copying whatever the last project used.
Choosing a Length
Using a 64-character URL-safe alphabet (A–Z, a–z, 0–9, -, _), each character carries
exactly 6 bits:
| Length | Bits | 1% collision chance at |
|---|---|---|
| 8 | 48 bits | ~2.4 million IDs |
| 10 | 60 bits | ~152 million IDs |
| 12 | 72 bits | ~9.7 billion IDs |
| 16 | 96 bits | ~4 × 10¹³ IDs |
| 21 | 126 bits | Beyond any realistic volume |
Alphabet Choices Matter
- URL-safe. Avoid characters that need percent-encoding, or the ID stops being short the
- Case sensitivity. A case-sensitive ID halves in strength if anything in your stack
- Ambiguous characters. If a human will ever read one aloud or type it from a printed
0, O, I, l and 1 is worth the small entropy loss.Do Not Truncate a Hash
A common shortcut is to hash something and take the first eight characters. That produces a deterministic ID — the same input always yields the same output — which leaks whether two records share an input and invites enumeration. If you want a random ID, generate random bytes.
Sequential Alternatives
Where ordering matters, ULID and UUIDv7 encode a timestamp in the high bits and keep randomness in the low bits. You get sortability and index locality at the cost of revealing roughly when the ID was created, which is sometimes fine and sometimes a leak.
Generated Locally
Bytes come from crypto.getRandomValues(), so the identifiers are unpredictable and never
leave your browser.
The Avalanche Effect
A one-character change produces a completely different digest — not a similar one. That property is what makes a hash useful as a fingerprint:
| Input | MD5 | CRC32 |
|---|---|---|
hello | 5d41402abc4b2a76b9719d911017c592 | 3610a686 |
hello. | d94c10e437d18531e122ed0b45badd2a | 0a39d4f1 |
Hello | 8b1a9953c4611296a827abf8c47804d7 | f7d18982 |
hello and Hello differ by one bit of one byte, and share no part of their output.
RIPEMD-160 of hello is 108f07b8382412612c048d07d13f814118445acd, and of Hello is
d44426aca8ae0a69cdbc4021c64fa5ad68ca32fe — same story.Digest Length and Collision Resistance
| Algorithm | Output | Birthday bound | Status |
|---|---|---|---|
| CRC32 | 32 bits | ~77,000 values | Checksum only |
| MD5 | 128 bits | 2⁶⁴ in theory | Broken — collisions in seconds |
| SHA-1 | 160 bits | 2⁸⁰ in theory | Broken — SHAttered, 2017 |
| RIPEMD-160 | 160 bits | 2⁸⁰ | No practical attack |
| SHA-256 | 256 bits | 2¹²⁸ | Current standard |
| SHA-512 | 512 bits | 2²⁵⁶ | Standard, faster on 64-bit |
Never Hash a Password With These
A general-purpose hash is designed to be fast, which is exactly wrong for passwords: speed helps the attacker. Use a deliberately slow KDF — bcrypt, scrypt or Argon2id — with a per-password salt. A GPU tries billions of SHA-256 guesses a second and a few thousand bcrypt guesses a second, and that gap is the entire defence.