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.

Reviewed 10 min read

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.

SourceEntropy behind itFitsFails
Platform CSPRNGKernel pool, reseededTokens, keys, salts, UUIDsMillions per second in a hot loop
Math.randomSmall state PRNGCosmetic jitter, samplingAnything an attacker can guess
Hash of a nameInput entropy onlyDeterministic ids, idempotencyGuessable names, uniqueness at scale
Time plus counterOrdering, no secrecySort keys, log sequencePredictability, clock regressions
Seeded PRNGSeed bits onlyReproducible fixturesProduction 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.

VersionRandom bitsSorts by timeDiscloses
v114-bit clock sequenceNo, node field firstMAC address and timestamp
v3 / v5None, the name is hashedNoNothing, but names are enumerable
v4122NoNothing
v614-bit clock sequenceYes, timestamp reorderedTime to 100 nanoseconds
v774Yes, by millisecondCreation time to the millisecond
v8Up to 122, chosen by youDepends on your layoutWhatever 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.

RuleWhat it appears to buyWhat it costs
Require upper, digit, symbolA wider alphabetNarrower patterns, answered predictably
Exclude ambiguous glyphsFewer transcription errors0.12 bits per character, 2.4 at twenty
Cap length at 16 or 20Predictable storageSilent truncation of a stronger secret
Rotate every 90 daysContained exposureSmaller variants of the previous value
Blocklist breached valuesCoverage of the real attackerOne 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.

LevelCodewords recoverableByte capacity at v40Choose when
LAbout 7 percent2,953Large print, short distance, dense payload
MAbout 15 percent2,331Everyday print and on-screen defaults
QAbout 25 percent1,663Handled, curved or lightly soiled surfaces
HAbout 30 percent1,273Logo 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.

NeedChooseFailure it catches
Layout onlyFixed lorem ipsumLine height, truncation, column overflow
Parser hardeningSeeded multilingual corpusNFC against NFD, RTL, 3-byte CJK, long compounds
Reproducible bugCommitted seed or keyed synthesisFixtures that cannot be regenerated
Network examplesRFC 5737 and RFC 3849 blocksRoutes that resolve to a real host
Contact examplesexample.test and 555-01xxMail 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.

ClaimWhat it assumesBreaks when
122 bits, collisions are impossibleUniform, independent drawsA 32-bit seeded PRNG sits behind it
Unique across all systemsNo shared stateA snapshot resumes one stream
First 8 characters are enoughThe full 128-bit space32 bits collide near 77,000 values
v7 is unique per millisecond74 random bits per drawA counter wraps inside one millisecond
Hash collisions will not happenA collision-resistant hashTruncation 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.

CheckCheap testWhat it catches
Source of bitsRead the call, not the outputA PRNG posing as a CSPRNG
Shape validityVersion and variant nibblesZeroed or hand-built ids
Round tripParse, re-emit, compare bytesCase folding, padding, zero-width spaces
Length against storageColumn width versus generated lengthSilent truncation
UniquenessA unique index, then a duplicate insertThe 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.randomBytes or crypto.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.

Try it now