DEVTOOLBOX / GUIDES this guide
UUID v4 or v7, password length, QR error correction: choosing a generator by criterion
Generated values fail quietly: a token from a weak PRNG still looks random, a truncated password still looks long, a printed QR code still looks scannable. This guide puts the entropy source ahead of the output format, then settles UUID version, password length, QR error-correction level and fixture realism with criteria, costs and the points where a choice stops being reversible.
Randomness is not one thing: entropy source before output format
Two values can look equally random and carry forty bits or a hundred and twenty-two, because the shape on screen says nothing about where the bits came from. Hex, Base64, hyphenated UUID strings and word lists are presentation. A generator is only as strong as the seed behind it and the state it advances.
Start with the platform pool. crypto.randomBytes in Node and crypto.getRandomValues in browsers read it directly: the kernel CSPRNG, seeded from device and interrupt timing; one getRandomValues call returns at most 65,536 bytes, so larger secrets loop. Math.random is a PRNG whose algorithm the ECMAScript specification deliberately leaves open, and V8 implements it with xorshift128+, whose state a handful of outputs recover. A hash of a name or a counter is deterministic by design. Time plus a counter is ordered but reproducible.
Apply the criterion, then price it. If the value must be unguessable — session tokens, reset links, salts, API keys — only the CSPRNG qualifies, and anything else turns a 128-bit secret into a puzzle. If the value only has to stay distinct inside one dataset, a counter beats randomness because it cannot collide at all. If it must be reproducible for a test, a seeded generator is the only honest option, and the seed joins the fixture contract. Speed is a red herring at human scale: a CSPRNG call costs microseconds, so ten thousand ids take tens of milliseconds.
The irreversible point is the first write. A token drawn from Math.random cannot be recalled from the mail, the log line or the browser tab it already reached, and issuing a replacement does not unissue the original.
| Source | Entropy behind it | Fits | Fails |
|---|---|---|---|
| Platform CSPRNG | Kernel pool, reseeded | Tokens, keys, salts, UUIDs | Millions per second in a hot loop |
| Math.random | Small state PRNG | Cosmetic jitter, sampling | Anything an attacker can guess |
| Hash of a name | Input entropy only | Deterministic ids, idempotency | Guessable names, uniqueness at scale |
| Time plus counter | Ordering, no secrecy | Sort keys, log sequence | Predictability, clock regressions |
| Seeded PRNG | Seed bits only | Reproducible fixtures | Production secrets |
Pick the row from the threat, never from the output format.
UUID versions: why v4 and v7 fail at different jobs
RFC 4122 defined versions 1 to 5; RFC 9562 adds 6, 7 and 8 without changing the 128-bit frame, of which 6 bits are fixed: 4 for the version and 2 for the variant. What is left is the whole story of a version. v4 spends all of it on randomness, 122 bits. v7 spends 48 on Unix milliseconds and leaves 74 random.
Choose v4 when the id must reveal nothing. It is the only version whose bits are unrelated to time, host or payload, which is why it belongs in URLs that should not advertise when an account appeared. Choose v7 when the id is also the sort key: sequential keys append to the right edge of a B-tree index instead of scattering across every page, and the win is largest on tables with hundreds of millions of rows. The price is disclosure, since a v7 id states its creation time to the millisecond, in the clear, permanently.
The other versions answer narrower questions. v3 and v5 hash a namespace plus a name, so the same pair yields the same id on every machine: correct for idempotent imports, wrong wherever names are guessable, because a guess confirms itself. v1 and v6 pack a 60-bit timestamp counting 100-nanosecond intervals from 1582-10-15, and v1 as originally specified also carried the host MAC address; RFC 9562 directs implementations to fill that node field with random bits, since a MAC address identifies hardware across networks.
Storage is the irreversible half. A native uuid column or BINARY(16) stores 16 bytes; varchar(36) holds 36 characters and compares under a collation, and moving between them later rewrites the table and every index on it. Note also that six of those bytes are fixed, so the string carries 122 bits and not 288.
| Version | Random bits | Sorts by time | Discloses |
|---|---|---|---|
| v1 | 14-bit clock sequence | No, node field first | MAC address and timestamp |
| v3 / v5 | None, the name is hashed | No | Nothing, but names are enumerable |
| v4 | 122 | No | Nothing |
| v6 | 14-bit clock sequence | Yes, timestamp reordered | Time to 100 nanoseconds |
| v7 | 74 | Yes, by millisecond | Creation time to the millisecond |
| v8 | Up to 122, chosen by you | Depends on your layout | Whatever you encode in it |
The trade is constant: order and traceability against disclosure.
Password generation: length, alphabet, and the rules that weaken it
Password strength is arithmetic before it is advice: length times the base-two log of the alphabet. Twenty characters drawn uniformly from 62 symbols are 20 times 5.95, about 119 bits; twelve are 71.5. Six words from the 7,776-word Diceware list are 6 times 12.92, about 77.5 bits, so four words at 51.7 beat eight characters from 62 symbols at 47.6.
Length is the only free lever. Each extra character multiplies the space by the alphabet size, so a 62-symbol draw gains 5.95 bits per character, while widening to all 94 printable ASCII characters adds 0.6 bits per character. Shrinking the alphabet to avoid ambiguous glyphs — dropping 0, O, 1, l, I — leaves 57 symbols and costs 0.12 bits per character, about 2.4 bits at twenty characters: cheap insurance against transcription errors, a real loss at eight characters.
The rules meant to save weak passwords weaken strong ones. NIST SP 800-63B forbids composition requirements and forced rotation, sets 8 characters as the minimum while requiring support for at least 64, forbids truncation, and requires checking new values against breached and common ones. Composition does not widen the alphabet in practice; it narrows the search, because every user answers the requirement the same way.
The dangerous defaults are silent. bcrypt hashes at most 72 bytes and ignores the rest without an error, so a long passphrase is defended by its prefix alone; a legacy varchar(16) column cuts a 40-character secret at the application layer. Generated length above the limit adds nothing and conceals the loss. Issuing is irreversible, because copies already live in systems you do not control.
| Rule | What it appears to buy | What it costs |
|---|---|---|
| Require upper, digit, symbol | A wider alphabet | Narrower patterns, answered predictably |
| Exclude ambiguous glyphs | Fewer transcription errors | 0.12 bits per character, 2.4 at twenty |
| Cap length at 16 or 20 | Predictable storage | Silent truncation of a stronger secret |
| Rotate every 90 days | Contained exposure | Smaller variants of the previous value |
| Blocklist breached values | Coverage of the real attacker | One lookup on every set and login |
Composition rules shrink the space they claim to widen.
QR codes: error correction levels and what each level costs
A Model 2 QR symbol under ISO/IEC 18004 carries Reed-Solomon codewords in interleaved blocks, and the level fixes how many of those codewords may be destroyed: L recovers about 7 percent, M 15, Q 25, H 30. The figure is a fraction of codewords, not of printed area, and it is fixed when the symbol is encoded.
The cost is capacity. At version 40, the largest symbol at 177 by 177 modules, byte mode holds 2,953 bytes at L, 2,331 at M, 1,663 at Q and 1,273 at H, so L to H gives up more than half. The bill is paid either by a larger symbol or by smaller modules in the same footprint, and the second fails once a module is too small for the camera at the intended distance. Version n is 17 plus 4n modules, so version 1 is 21 by 21 and version 10 is 57 by 57.
Choose L for a dense payload printed large and scanned close, where capacity is the binding constraint. Choose H for anything that will be abused: a logo over the centre, a sticker on a laptop lid, a poster behind glass in daylight. Higher levels also split data into more blocks, which localises damage, so a thumb over the middle of an H symbol destroys a smaller share of any one block.
The defects that actually bite are elsewhere. A quiet zone of 4 modules on every side is required and routinely cropped by design tools. Lowercase text forces byte mode, since alphanumeric mode holds only uppercase letters, digits, space and eight punctuation marks at 11 bits per two characters, near 5.5 bits per character against 8 for bytes. Non-ASCII needs UTF-8 bytes and an ECI designator. All three are decisions taken before printing, and no level can repair them afterwards.
| Level | Codewords recoverable | Byte capacity at v40 | Choose when |
|---|---|---|---|
| L | About 7 percent | 2,953 | Large print, short distance, dense payload |
| M | About 15 percent | 2,331 | Everyday print and on-screen defaults |
| Q | About 25 percent | 1,663 | Handled, curved or lightly soiled surfaces |
| H | About 30 percent | 1,273 | Logo overlay, stickers, outdoor posters |
Raising the level spends capacity; it never buys durability for free.
Lorem ipsum and test data: reproducible versus realistic
Lorem ipsum is a fixed corpus, not a random one: the familiar opening is a scrambled passage from Cicero, De finibus bonorum et malorum, and every generator serves a slice of the same text. That is exactly right for judging line height, column width and truncation, and exactly wrong for anything that depends on content, such as sorting, search, validation or translation.
Separate the two jobs. If the fixture proves that a layout holds, fixed Latin is the cheapest tool and stays stable across runs. If it proves that a parser survives its input space, the corpus has to vary where real data varies: diacritics in NFC and NFD, right-to-left runs, CJK at 3 bytes per character in UTF-8, zero-width joiners inside emoji, German compounds and single-character names. Latin lorem produces none of them, which is why a form that looks finished in a demo fails on the first customer.
Reproducibility is built, not hoped for. Seed a PRNG and commit the seed beside the fixture, or synthesise each field from a stable key such as the record id, so a failure in continuous integration can be replayed. A fixture that fails once and never again costs a day of guessing, which is the price of skipping the seed.
The irreversible mistake is realistic-looking data that is real. A production dump copied into staging cannot be unleaked, and the incident is not undone by deleting the copy. Documentation ranges exist for this: RFC 5737 sets aside 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24, RFC 3849 sets aside 2001:db8::/32, RFC 2606 reserves .test, .example and .invalid, and 555-0100 through 555-0199 is the reserved fiction block. RFC 1918 space is not a substitute, because 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 route inside real networks and can shadow a tunnel route.
| Need | Choose | Failure it catches |
|---|---|---|
| Layout only | Fixed lorem ipsum | Line height, truncation, column overflow |
| Parser hardening | Seeded multilingual corpus | NFC against NFD, RTL, 3-byte CJK, long compounds |
| Reproducible bug | Committed seed or keyed synthesis | Fixtures that cannot be regenerated |
| Network examples | RFC 5737 and RFC 3849 blocks | Routes that resolve to a real host |
| Contact examples | example.test and 555-01xx | Mail or calls reaching a stranger |
Realism is a test input; reproducibility is a debugging tool.
Uniqueness claims: what a collision probability actually assumes
A uniqueness claim is a statement about four assumptions: a known total space, a uniform draw, independent draws, and values that are never truncated or rewritten. Drop one and the printed digits keep their length while the guarantee collapses, which is the failure mode that survives code review because nothing looks wrong.
Birthday arithmetic settles the rest. The chance of at least one collision among n draws from N values is about n squared divided by 2N while n stays small against N, and the fifty percent point sits near 1.177 times the square root of N. For v4, N is 2^122, so half chance arrives around 2^61, and one billion ids, at 10^18 over twice 5.3 times 10^36, land near 10^-19.
The assumptions break in mundane ways. A PRNG seeded from 32 bits reaches only 4.29 billion sequences no matter how long its output is, so the effective space is 2^32. Truncation is the second break: keeping the first 8 hex characters of a UUID leaves 32 bits, where a fifty percent collision arrives near 77,000 values and one percent near 9,300. Independence fails when state is cloned, as a forked process or a restored snapshot resuming one PRNG stream. v7 collides only inside a single millisecond, where its 74 random bits apply, which is why RFC 9562 defines counter methods for high rates.
No probability replaces a constraint. A unique index or a primary key is what makes duplicates impossible; the id generator only makes them unlikely. Keep the retry path on constraint violation, and treat the first duplicate error as evidence about the generator rather than about luck.
| Claim | What it assumes | Breaks when |
|---|---|---|
| 122 bits, collisions are impossible | Uniform, independent draws | A 32-bit seeded PRNG sits behind it |
| Unique across all systems | No shared state | A snapshot resumes one stream |
| First 8 characters are enough | The full 128-bit space | 32 bits collide near 77,000 values |
| v7 is unique per millisecond | 74 random bits per draw | A counter wraps inside one millisecond |
| Hash collisions will not happen | A collision-resistant hash | Truncation or a non-cryptographic hash |
Every claim is conditional on a generator you did not write.
A checklist before trusting a generated value
Run the checks in the order that eliminates the most first, and stop at the first failure rather than collecting all of them. Source, then shape, then round trip, then storage width, then the constraint. Each step is cheaper than the one after it, and the last one is the only one that guarantees anything.
The artifacts that look normal are the ones to test. A UUID whose fifteenth character is not the version nibble, or whose twentieth is not 8, 9, a or b, is not a UUID that any conforming library wrote. Uppercase hex parses in some systems and not others. Changing + to - and / to _ moves a token between Base64 and Base64url, and the two fail each other silently. A value pasted from a document carries a trailing newline or a zero-width space that survives until a parser rejects it in another service.
Prefer a round trip to an inspection: parse the value, re-emit it, and compare bytes. That single test covers case folding, padding, whitespace and truncation at once, and it is the one most often skipped because the source looked trustworthy.
Two costs stay after the checklist passes. A secret that reached a log is compromised whether or not the log was deleted, so the only remedy is rotation, and rotation is a deployment. A generated value handed to a partner cannot be recalled, so the moment of issue is the last moment the decision is still yours.
- Read the call that produces the value, not the value: a CSPRNG function name in the output says nothing about the code path that ran.
- Validate version and variant nibbles whenever the value claims to be a UUID, and reject anything that fails rather than repairing it.
- Round trip every generated value through the parser that will consume it, and compare bytes rather than strings.
- Compare the generated length against the column width and the receiving form field before issuing, since both truncate quietly.
- Enforce uniqueness with a unique index and a retry on conflict, and log the conflict so a weak generator becomes visible.
- Keep generation reproducible where it is auditable: commit the seed, the alphabet and the length beside the fixture.
| Check | Cheap test | What it catches |
|---|---|---|
| Source of bits | Read the call, not the output | A PRNG posing as a CSPRNG |
| Shape validity | Version and variant nibbles | Zeroed or hand-built ids |
| Round trip | Parse, re-emit, compare bytes | Case folding, padding, zero-width spaces |
| Length against storage | Column width versus generated length | Silent truncation |
| Uniqueness | A unique index, then a duplicate insert | The belief that arithmetic replaced a constraint |
Four of these are one-line tests; run them before the value ships.
Frequently asked questions
- Should I use UUID v4 or v7 for a new primary key?
- Pick v7 when inserts dominate and the id is the clustered key, because 48 bits of milliseconds append to the index instead of scattering writes across pages. Pick v4 when the id appears in public URLs and the creation time must not leak. Both are 128 bits; v4 keeps 122 random bits and v7 keeps 74, so v7 trades collision headroom and disclosure for locality.
- Is a UUID generated with Math.random acceptable as a token?
- No. The output length proves nothing about the entropy behind it, and a small-state PRNG such as the xorshift128+ used by V8 can be reconstructed from a handful of observed outputs. A reset token from it is guessable regardless of how many characters it prints. Use
crypto.randomBytesorcrypto.getRandomValues, and treat every token already issued as compromised. - How long should a generated password be?
- Length beats alphabet once you are past a sensible set of symbols. Twenty characters over 62 symbols is about 119 bits; with ambiguous glyphs removed, the same length is roughly 116. NIST SP 800-63B sets 8 characters as a floor and requires support for 64, but the practical rule is to generate longer than any downstream limit, since bcrypt truncates at 72 bytes and some forms cut at 16 without an error.
- Does a higher QR error-correction level always make a code more reliable?
- No. H recovers about 30 percent of codewords against 7 percent at L, but the same payload then needs more modules, so at a fixed print size each module gets smaller and scanning range drops. Choose H when damage is expected, such as a logo overlay or a sticker, and choose L or M for dense payloads on large clean surfaces. Never crop the 4-module quiet zone to buy space back.
- Can I use realistic production-like data in test fixtures?
- Only if it never was real. Copying a production dump into staging cannot be undone, and deleting the copy does not unleak it. Synthesise instead, and keep addresses in reserved ranges: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 and 2001:db8::/32 never route, while RFC 1918 space does route inside real networks and can shadow a tunnel in a test.