Under the hood · 5 min read
Modulo bias and rejection sampling, worked through
A small arithmetic trap in password generators, worked through with real numbers: where the bias comes from, how big it is, and how to remove it.
A password generator gets random bits from the system and needs a random choice among some number of options: 94 printable characters, 62 letters and digits, 10 digits, 7,776 words. The obvious way to convert one into the other is the remainder operator: take a random number and compute random % n. Unless the range of the random number is an exact multiple of n, that is subtly unfair. The unfairness is called modulo bias.
It is easy to fix. It is also easy to miss, because the output still looks random.
Worked example: one byte, 94 characters
Say the generator draws one random byte, a value from 0 to 255, each with probability 1/256, and wants one of 94 characters.
256 divided by 94 is 2 with remainder 68: 256 = 2 × 94 + 68.
Now count how many byte values land on each result of byte % 94:
| Result of byte % 94 | Byte values that produce it | Count | Probability |
|---|---|---|---|
| 0 to 67 | r, r + 94, r + 188 | 3 | 3/256 = 1.17% |
| 68 to 93 | r, r + 94 | 2 | 2/256 = 0.78% |
For example, result 5 comes from bytes 5, 99 and 193. Result 70 comes from bytes 70 and 164 only, because 70 + 188 = 258 is beyond 255. The first 68 characters are each 1.5 times as likely as the last 26.
If the 94 characters are listed in ASCII order, starting at !, the favored 68 run from ! through d, and the disfavored 26 are e through ~, which includes most of the lowercase letters.
How much does it matter?
Measured as entropy, the loss looks small. A uniform choice from 94 gives log2(94) = 6.555 bits per character. The biased distribution gives 6.534 bits, a loss of 0.021 bits per character, or about 0.41 bits across a 20-character password.
The more telling view is what an attacker can do with it. Knowing the bias, they try passwords made of favored characters first. The single most likely 20-character password under this bias is (3/256)^20 likely, compared with (1/94)^20 for any password under a fair generator. The ratio is (3 × 94 / 256)^20 = 1.1016^20, about 6.9. The most likely passwords are almost seven times more likely than they should be, and the guaranteed-uniform strength figure the generator displays is no longer exactly true.
None of this makes a 20-character password weak. It makes the generator’s claim inaccurate, and a generator that publishes its strength calculation should be exactly right.
Rejection sampling
The fix is to throw away the uneven part. With one byte and n = 94:
- Compute the largest multiple of 94 that fits in 256: 256 − (256 mod 94) = 256 − 68 = 188.
- Draw a byte. If it is 188 or more, discard it and draw again.
- Otherwise return byte % 94.
Bytes 0 to 187 cover each of the 94 results exactly twice (r and r + 94), so every result has the same probability. The discarded values 188 to 255 are the ones that caused the extra third chance for results 0 to 67.
The cost is the occasional redraw. With one byte, 68 out of 256 draws are rejected, 26.6%, so on average it takes 256 / 188 = 1.36 draws per character. That is cheap, and it can be made far cheaper with a wider random number.
This site’s method: 32 bits at a time
This site’s generators draw 32-bit values from crypto.getRandomValues and use the same rule with a much bigger range:
- Draw a 32-bit value x, from 0 to 4,294,967,295.
- If x ≥ 2^32 − (2^32 mod n), reject it and draw again.
- Otherwise return x mod n.
For n = 94, 2^32 mod 94 = 42, so the threshold is 4,294,967,296 − 42 = 4,294,967,254. Only the top 42 values are rejected. The chance of a redraw is 42 / 2^32, about 9.8 × 10^-9, or roughly 1 in 100 million.
In JavaScript the idea fits in a few lines:
function uniformBelow(n) {
const limit = 2 ** 32 - (2 ** 32 % n);
const buf = new Uint32Array(1);
while (true) {
crypto.getRandomValues(buf);
if (buf[0] < limit) return buf[0] % n;
}
}
The loop is not a performance concern. It almost never runs twice, and even with the one-byte version it would rarely run more than a few times.
Is it worth it at 32 bits?
Without rejection, 32-bit values mod 94 would give 42 of the characters 45,691,142 chances each and the other 52 only 45,691,141. That is a relative difference of about 2.2 × 10^-8, far too small to exploit. Rejection still costs nothing and turns “almost uniform” into “exactly uniform,” which is what lets the site state each password’s strength as a plain calculation.
The same problem in other places
Modulo bias appears wherever a random source with one range is squeezed into another:
- Floating-point scaling.
Math.floor(Math.random() * n)has a similar, smaller unevenness from the finite number of floating-point values, on top ofMath.randomnot being a cryptographic source at all (see how password generators work). - Dice. A six-sided die cannot fairly pick one of 10 digits in one roll. Two rolls give 36 equally likely outcomes; 36 is not a multiple of 10, so the fair method keeps outcomes 1 to 30 (three per digit) and rerolls on 31 to 36. That is rejection sampling by hand. The EFF wordlist avoids the problem by having exactly 6^5 = 7,776 words, so five dice map onto it with no waste (see diceware with real dice).
- Picking words from a list. A wordlist of 7,776 words is not a power of two, so a software generator using random bits needs rejection here too. 2^32 mod 7,776 = 2,560, so only the top 2,560 of the 4.29 billion values are rejected.
Other unbiased methods exist, including ones that use multiplication instead of division, but they all end up rejecting some small slice of the range. That is unavoidable: an exact uniform choice among n options cannot be made from a fixed number of random bits unless n is a power of two.
How you can check a generator
Bias of the one-byte size is visible with a simple experiment: generate a few million characters, count each one, and compare the counts with what a fair generator would give using a chi-square test. With one byte mod 94, favored characters would show up about 1.5 times as often as disfavored ones, which a test catches immediately. This site runs that kind of test over millions of samples at build time and publishes the results on the methodology page.
Sources
General explanation, not advice for a particular situation. Updated 2026-10-31.