The Code That Looks Fine and Isn't
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let token = '';
for (let i = 0; i < 32; i++) {
const bytes = new Uint8Array(1);
crypto.getRandomValues(bytes);
token += chars[bytes[0] % chars.length];
}
Cryptographic random source. Correct length. Reasonable character set.
And a bias that makes some characters roughly 14% more likely than others.
The problem is % chars.length. A byte holds 256 values. The character set has 36. 256 divided by 36 is 7 remainder 4 — so values 0 through 3 map to the first four characters an extra time, and those four characters appear more often than the other 32.
The bias isn't catastrophic on its own, but it's an entirely avoidable weakness in something whose only purpose is uniformity.
Modulo Bias, Precisely
The general rule: modulo introduces bias whenever the range of the random source isn't an exact multiple of the target range.
256 % 36 = 4 → biased
256 % 16 = 0 → no bias
256 % 64 = 0 → no bias
256 % 62 = 8 → biased (alphanumeric!)
Note that 62 — the alphanumeric character set — is biased. So is 58 (Base58), 36 (alphanumeric lowercase), and most character sets that aren't powers of two.
The correct approach: rejection sampling
Discard values that fall outside a clean multiple of the target range:
function randomIndex(max) {
const limit = Math.floor(256 / max) * max; // largest clean multiple
const bytes = new Uint8Array(1);
let value;
do {
crypto.getRandomValues(bytes);
value = bytes[0];
} while (value >= limit);
return value % max;
}
For a 36-character set, limit is 252. Values 252–255 are rejected and redrawn. The rejection rate is about 1.6% — a negligible cost for perfect uniformity.
Or use a power-of-two character set
If your character set size is a power of 2, modulo is unbiased by construction:
16 hex
32 Base32
64 Base64 / Base64url
This is one of several quiet arguments for hex or base64url tokens: the generation is bias-free without any extra logic.
Sizing a Token
Two different questions get conflated here, and they have different answers.
For security: entropy
How hard is this to guess?
entropy bits = length × log₂(charset size)
| Charset | Bits per char | Chars for 128 bits |
|---|---|---|
| Hex (16) | 4 | 32 |
| Base32 (32) | 5 | 26 |
| Alphanumeric (62) | 5.95 | 22 |
| Base64url (64) | 6 | 22 |
128 bits is the conventional target for security tokens — session identifiers, API keys, password reset tokens. It's far beyond brute-force reach and leaves margin.
64 bits is generally too little for anything security-sensitive. 256 bits is more than necessary for most purposes but costs nothing except length.
For uniqueness: the birthday problem
How many can I generate before a collision becomes likely?
This is a different calculation, and it's much less forgiving than intuition suggests.
The approximate rule: a collision becomes likely after roughly √(2ⁿ) values, where n is the entropy in bits. So an n-bit identifier gives you about n/2 bits of collision resistance.
| Entropy | Values before ~50% collision chance |
|---|---|
| 32 bits | ~77,000 |
| 64 bits | ~5 billion |
| 128 bits | ~2×10¹⁹ |
A 32-bit identifier — eight hex characters — collides after fewer than a hundred thousand values. That's a small table. Plenty of systems have used eight-character IDs and discovered this in production.
For identifiers at any real scale, 128 bits is the sensible floor. UUID v4 provides 122 bits of randomness, which is why it's the standard answer.
The Distinction That Matters Most
A random string for test data and one for a security token look identical in the output and are completely different artefacts.
Test data can come from Math.random(). It can be seeded for reproducibility — which is genuinely useful, since a failing test with a known seed can be replayed exactly. Predictability is a feature.
Security tokens must come from a CSPRNG. Predictability is the entire threat.
The failure mode is code written for the first purpose being reused for the second. A generateId() helper written for test fixtures, later called to produce password reset tokens, is a real and recurring pattern.
Practical defence: name them differently and keep them in different modules. randomTestId() and secureToken() are harder to confuse than two functions both called generateId.
Which Source to Use
Browser: crypto.getRandomValues()
Node.js: crypto.randomBytes() or crypto.randomUUID()
Python: secrets module (not random)
Go: crypto/rand (not math/rand)
Java: SecureRandom (not Random)
PHP: random_bytes() (not rand or mt_rand)
Ruby: SecureRandom
.NET: RandomNumberGenerator
The pattern is consistent: every language ships a fast statistical PRNG and a separate cryptographic one. The fast one is for simulations, sampling and test data. The cryptographic one is for anything an adversary would want to predict.
Math.random() in JavaScript is explicitly documented as not cryptographically secure. Its output is generated by an algorithm whose internal state can potentially be recovered from observed outputs.
The Random String Generator produces tokens of a chosen length and character set:
- Set the length.
- Choose the character set.
- Generate.
For learning, testing and low-stakes use this is fine. For production secrets, generate them in your own environment — a browser tool means the value travels through your clipboard and potentially your browser history, and you can't verify what the page does with it.
Choosing an Encoding
The character set affects more than entropy per character.
Hex — 16 characters, case-insensitive, no ambiguity, no special characters. Safe everywhere. Costs 33% more length than base64 for the same entropy.
Base64url — 64 characters including - and _. URL-safe, no padding needed if you generate a multiple of 3 bytes. The efficient default for tokens that appear in URLs.
Standard Base64 — includes +, / and =, all of which need percent-encoding in URLs. Avoid for anything URL-bound.
Alphanumeric (62) — no special characters at all, so it double-click-selects as one word and survives any context. Slightly awkward to generate without bias.
Base32 — case-insensitive and excludes visually ambiguous characters. The right choice when a human might read or type the value.
Practical rule: hex or base32 if a human touches it, base64url if it goes in a URL, either for machine-only values.
Storage and Handling
A few practices that matter as much as generation:
Store a hash, not the token. For API keys and long-lived tokens, store a hash of the value. A database compromise then doesn't hand over working credentials. Show the plaintext once at creation and never again.
Use constant-time comparison. Comparing tokens with ordinary string equality leaks their content through timing. Use timingSafeEqual, hmac.compare_digest or your language's equivalent.
Set expiry. A token that never expires is a permanent liability. Password reset tokens should live for minutes; session tokens for hours; API keys should at least be rotatable.
Single-use where applicable. Password reset and email verification tokens should be invalidated on use.
Never log them. Tokens in application logs are one of the most common accidental credential exposures.
Never put them in URLs where avoidable. URLs end up in browser history, server logs, referrer headers and analytics.
Practical Tips
Use rejection sampling or a power-of-two character set. Modulo alone is biased for most character sets.
Target 128 bits of entropy for security tokens and identifiers.
Remember the birthday bound. Collision resistance is half your entropy.
Keep test-data and security-token generation in separate functions with names that can't be confused.
Prefer randomUUID() where it fits — it's built in, correctly implemented, and removes the opportunity to get any of this wrong.
Generate production secrets locally, not in a browser tool.
FAQ
What is modulo bias? When the random source's range isn't an exact multiple of your character set size, some characters become more likely than others. Fixed by rejection sampling or a power-of-two character set.
How long should a token be? Aim for 128 bits of entropy — 32 hex characters, 22 base64url characters, or 22 alphanumeric characters.
Why is 32 bits not enough for IDs? The birthday bound. Collisions become likely after roughly 77,000 values, which is a small dataset.
Is Math.random() ever acceptable?
For test data, simulations and sampling, yes — and seeding it for reproducible tests is genuinely useful. Never for anything security-relevant.
Should I use UUID v4 or a random string? UUID v4 is a well-implemented standard with 122 bits of randomness. Use it unless you need a specific format or a shorter representation.
Should I store API keys as plaintext? No. Store a hash and display the plaintext only once at creation.
The Takeaway
Random string generation has two independent failure modes: a weak source, and biased selection from a good source. The first is well known; the second sits in a lot of code that looks correct. Rejection sampling or a power-of-two alphabet fixes it in three lines, and 128 bits of entropy covers both guessing and collisions.
Generate random strings and tokens free with the Random String Generator at sadiqbd.com — no sign-up, instant results.