Picking an identifier: UUIDs, short IDs and the ones that leak information
Use a UUID v4 for anything internal, a v7 when the identifier is the primary key of a large table, and a short code from a restricted alphabet when a person has to read it aloud. Sequential integers are the option to avoid on anything public.
UUID versions that matter
A UUID is 128 bits. Four carry the version and two the variant, so no version has all 128 to spend.
| Version | What it contains | Use it when |
|---|---|---|
| v1 | 60 bit timestamp, 14 bit clock sequence, 48 bit node ID (the MAC address) | Legacy data only |
| v3 / v5 | An MD5 or SHA-1 hash of a namespace and a name | Deterministic IDs are needed |
| v4 | 122 random bits, nothing else | Default, when nothing should be inferable |
| v7 | 48 bit millisecond timestamp, then 74 random bits | Keys where insert order matters |
v1 is the cautionary one. It embeds the creation time and, in most implementations, the MAC address of the machine that made it, which is how such identifiers helped trace the Melissa virus author in 1999. v7, standardised in RFC 9562 in 2024, publishes the creation time on purpose: its feature and its cost.
The collision question
A v4 UUID has 122 random bits, so there are 2^122 of them, roughly 5.3 x 10^36. What matters is the birthday bound, not the size of the space: the chance that any two of n values match is about n squared over 2^123. A trillion generated UUIDs carry about a one in ten trillion chance of a duplicate, and a coin flip's chance takes about 2.7 x 10^18 values, some 85 years at a billion per second.
"Not a practical concern" is still not "impossible", and the gap is never the arithmetic. Duplicates come from the generator: a fixed seed, a virtual machine cloned with its entropy pool, a container image shipping saved PRNG state. Uniqueness belongs to the random source, not the format, so keep the unique constraint on the column.
Ordering, index locality and leakage
Random identifiers scatter. In a B-tree index every insert lands in a random leaf page, so the working set is the whole index rather than its right edge, cache hit rates fall and pages split. A key that rises over time appends in one place, keeping the same few pages hot. That is why v7 and ULID exist; ULID is 48 bits of millisecond timestamp plus 80 random bits, written as 26 sortable characters of Crockford base32.
The cost is predictability: a time ordered ID publishes when its record was created, so a handful reveals sign-up rates and quiet hours. If that matters, use v4 and pay the index cost.
Sequential integers go further. /invoices/1041 says you have issued about a thousand invoices, and two IDs a week apart give your growth rate. Worse, an attacker can request 1040. If the server returns that record because it exists and the caller is logged in, without checking who owns it, that is a broken access control failure, catalogued as an insecure direct object reference. An unguessable ID is not an authorisation check, but it does stop anyone walking the table.
Slugs are identifiers too, leaking nothing about volume when built properly: accents folded, case lowered, punctuation collapsed to hyphens, as the Slug Generator does. Keep one stable after publication; changing it breaks inbound links.
Short IDs, alphabets and length
Every character of a random code is worth log2 of the alphabet size: 5.95 bits for base62, 5 for base32, 4 for hexadecimal. Length and alphabet set the collision probability for a given volume.
| Length (base62) | Distinct values | Bits | Collision chance within 1 billion IDs |
|---|---|---|---|
| 8 | 2.2 x 10^14 | 43.7 | Certain |
| 10 | 8.4 x 10^17 | 59.5 | About 45% |
| 12 | 3.2 x 10^21 | 71.5 | About 1 in 6,500 |
| 16 | 4.8 x 10^28 | 95.3 | About 1 in 100 billion |
That assumes each character is independently random; a prefix such as inv_ adds zero bits.
For anything a human reads, base62 is the wrong alphabet. 0 and O, 1 and l and I are transcription errors waiting to happen, and a case sensitive code cannot survive a phone call. Crockford base32 drops I, L, O and U and accepts either case on input, worth the extra character.
Where the random bits come from
Math.random() is not a cryptographic source. V8 implements it with xorshift128+, whose internal state is recoverable from a short run of outputs, after which every future value is predictable. Fine for a demo shuffle or a placeholder. Wrong for a session identifier, an API key, a reset link or a token.
// Browsers, Node 19+, Deno and Bun
const id = crypto.randomUUID();
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
Elsewhere: crypto.randomBytes in Node, secrets.token_urlsafe in Python, crypto/rand in Go, SecureRandom in Java, random_bytes in PHP. The UUID Generator produces cryptographically random v4 values and the Password Generator uses the same browser API; Random Numbers is for sampling, not secrets.
One trap when rolling your own: mapping a random byte onto a 62 character alphabet with % 62 skews towards the first eight characters; use rejection sampling.
Placeholder and sample data
Lorem ipsum is mangled Cicero, and it survives because it is unreadable: text that looks like language without being language lets a layout be judged on shape and line length rather than on its sentences, which is what the Lorem Ipsum generator produces. Swap in realistic copy before sign-off, because placeholder Latin hides the overflow a real headline will cause.
Fabricated data needs one rule: it must never be mistakable for the real thing.
- Card numbers come from a payment provider's published test range, or fail the Luhn check outright. A random 16 digit number that passes Luhn belongs to somebody.
- National identifiers use reserved ranges. US Social Security numbers beginning 000, 666 or 900 to 999 are never issued.
- Use
example.comand its siblings, reserved by RFC 2606, and the documentation block 192.0.2.0/24.
Test data reaches production more often than expected: a seed script pointed at the wrong database, a placeholder left in an email template sent to the whole list. Fake records should look obviously fake, so a human catches one before support acts on it.
QR codes in brief
A QR code is a grid of modules, versions 1 to 40, from 21x21 up to 177x177. Payload length drives the version, and at a fixed printed size a higher version means smaller modules, needing better printing and a closer scan. Shorten the URL before feeding it to the QR Code Generator, and note that alphanumeric mode is uppercase only, so HTTPS://EXAMPLE.COM encodes smaller than the lowercase form.
Error correction has four levels, recovering roughly 7% (L), 15% (M), 25% (Q) and 30% (H) of a damaged code. Redundancy costs capacity, so a higher level pushes the same payload into a denser version. M is the usual default; choose Q or H for small print, curved surfaces, or a logo over the middle, which works because the redundancy absorbs it.
Two physical requirements decide whether a code scans. The quiet zone is a clear margin of four modules on every side, used by the decoder to find the boundary, so a code butted against a border or photograph often fails. Contrast must be genuinely dark on genuinely light: pale blue on white is a common failure, and many decoders refuse inverted codes.