Rejection sampling, explained
Every generator on this site turns random bytes into letters. The bytes come from the Web Crypto
API (crypto.getRandomValues), the same operating-system-backed source browsers use
for encryption keys and secure tokens. One step decides whether the result is honest: how a
number from 0 to 255 becomes a position in a pool of 26, or 5, or 52, without favoring any letter. Every figure in this guide is computed
from the pool sizes, and every worked draw is produced by the real engine at build time from a
scripted byte source, so the page is reproducible; live draws use crypto.getRandomValues instead. The methodology page
states the construction; this page shows the arithmetic behind it. For the full path from byte to
letter, see how a random letter is drawn.
The mismatch: 256 byte values, 26 letters
A byte is 8 bits, so it holds one of 256 values, 0 through 255, each equally likely from a cryptographic source. The English pool has 26 entries, so a draw must produce a number from 0 to 25. The ranges do not fit: 256 divided by 26 is 9 with 22 left over. Any rule that assigns every byte value to a letter must hand some letters more byte values than others. A fair draw therefore needs a rule allowed to assign no letter to some byte values — one that can say "this byte does not count; draw another".
What the remainder shortcut gets wrong
The obvious rule keeps the remainder of the byte divided by the pool size. A byte modulo 26 always lands between 0 and 25, so it always names a letter. Count where the bytes go, though. Walking 0, 1, 2, … around the alphabet completes 9 full laps — bytes 0 through 233 — in which every letter receives exactly 9 byte values. The remaining 22 bytes, 234 through 255, begin one more lap and stop part-way, on A through V. So A–V each receive 10 byte values and W–Z receive 9: probabilities of 10/256 (3.91%) and 9/256 (3.52%) against the 1/26 (3.85%) a fair draw owes every letter. The ratio 10/9 makes A–V about 11% more likely than W–Z. The bias is structural and invisible: every result is still a valid letter. Its size depends on the pool:
| Pool | Size | 256 ÷ size | Favored entries | Extra likelihood |
|---|---|---|---|---|
| English alphabet | 26 | 9 remainder 22 | 22 of 26 | 11.1% |
| Vowels | 5 | 51 remainder 1 | 1 of 5 | 2.0% |
| Consonants | 21 | 12 remainder 4 | 4 of 21 | 8.3% |
| Greek alphabet | 24 | 10 remainder 16 | 16 of 24 | 10.0% |
| Both cases | 52 | 4 remainder 48 | 48 of 52 | 25.0% |
None of the five sizes divides 256 evenly, so the shortcut is biased for all of them: mildest for the "vowels" pool at 2.0%, widest for the "both cases" pool at 25.0%. The engine never divides.
Step one: keep only the bits that cover the pool
Instead, the engine asks how many bits are needed to write every position in the pool. For 26 letters that is 5 bits, covering 0 through 31 — 26
positions with 6 to spare — where 4 bits would reach only 15. It keeps the low 5 bits of the byte and discards the top 3 by masking against 31 (binary 00011111). This step is perfectly even: because 32 divides 256, every masked value from 0 to 31 is produced by exactly 8 of the 256 bytes. Masking shrinks the range without tilting it.
A byte whose high bits are discarded. Pool of 26; 5 bits kept; mask 31.
-
Byte 71 (
01000111) → low 5 bits00111= 7 → accepted
Letter drawn: H
Build-time draw through the real engine with scripted bytes chosen for illustration; live
draws take their bytes from crypto.getRandomValues.
Its high bits are simply dropped; the masked value already sits inside the pool, so no second byte is needed.
Step two: reject anything at or beyond the pool size
Masking leaves 32 equally likely values, but the pool has only 26. The values 26 through 31 — 6 of them — point past the end of the pool. The remainder shortcut would fold them back onto A–F, which is where the bias above comes from. The engine instead discards the value and draws a fresh byte, repeating until a masked value below 26 appears.
One rejection, then a letter. Pool of 26; 5 bits kept; mask 31.
-
Byte 30 (
00011110) → low 5 bits11110= 30 → rejected, not below 26 -
Byte 12 (
00001100) → low 5 bits01100= 12 → accepted
Letter drawn: M
Build-time draw through the real engine with scripted bytes chosen for illustration; live
draws take their bytes from crypto.getRandomValues.
A masked value exactly equal to the pool size. Pool of 26; 5 bits kept; mask 31.
-
Byte 250 (
11111010) → low 5 bits11010= 26 → rejected, not below 26 -
Byte 3 (
00000011) → low 5 bits00011= 3 → accepted
Letter drawn: D
Build-time draw through the real engine with scripted bytes chosen for illustration; live
draws take their bytes from crypto.getRandomValues.
The second trace matters for the wording "at or beyond": a masked value of exactly 26 is rejected, because positions in a pool of 26 run from 0 to 25 and position 26 does not exist.
Why the result is exactly uniform
Together the two steps turn the uniformity claim into a counting argument. Each of the 32 masked values arises from exactly 8 byte values; the engine accepts 26 of them and rejects 6. Among accepted draws every letter therefore corresponds to the same 8 byte values and carries the same probability, exactly 1/26. No letter is heavier, because nothing was wrapped around.
This is what the methodology page means by saying the claim rests on the construction, not on statistical testing after the fact. Tallying letters over thousands of draws can report only that the observed shares came out close to 1/26; it cannot tell an exactly fair construction from a slightly tilted one. The counting argument settles every draw in advance. What can be checked mechanically is the mapping itself, and that is what scripted bytes are for: the build of this page asserts, by running the engine on the bytes above, that a masked 30 is rejected and that byte 12 is M; the engine's test suite pins the byte-to-letter mapping, rejection path included, with scripted byte sources of its own. Neither tallies a distribution — that is left to the construction.
Every letter in the active pool is exactly equally likely — the engine never weights draws by English text frequency or any other distribution.
That sentence is exported from the engine source and rendered verbatim; rejection sampling is what makes it true, and it is the only distribution the engine implements.
The same construction for every pool
The pool size is the only input; bit width, mask and rejected range follow from it, and the loop does not change.
Vowel pool. Pool of 5; 3 bits kept; mask 7.
-
Byte 6 (
00000110) → low 3 bits110= 6 → rejected, not below 5 -
Byte 13 (
00001101) → low 3 bits101= 5 → rejected, not below 5 -
Byte 9 (
00001001) → low 3 bits001= 1 → accepted
Letter drawn: E
Build-time draw through the real engine with scripted bytes chosen for illustration; live
draws take their bytes from crypto.getRandomValues.
Both cases. Pool of 52; 6 bits kept; mask 63.
-
Byte 60 (
00111100) → low 6 bits111100= 60 → rejected, not below 52 -
Byte 90 (
01011010) → low 6 bits011010= 26 → accepted
Letter drawn: a
Build-time draw through the real engine with scripted bytes chosen for illustration; live
draws take their bytes from crypto.getRandomValues.
The vowel pool needs only 3 bits, so bytes are masked against 7 and the rejected values are 5 through 7; it takes 2 rejections before a byte lands. The mixed-case trace is the point the "both cases" option turns on: the pool was doubled before the draw, so the sampler works on 52 entries with 6 bits and lands on index 26, the first lowercase entry. Each of the 52 forms has probability 1/52; there is no uppercase draw followed by a coin flip.
What rejection costs
A rejected byte is wasted, so a letter may take several attempts; the cost is bounded by the choice of bit width. Because the engine keeps the fewest bits that cover the pool, the pool always fills more than half of the masked range — for the English alphabet, 26 of 32 values, an acceptance rate of 81.25%. Each attempt succeeds independently with that probability, so a letter takes 32/26 ≈ 1.23 bytes on average, about 0.23 wasted per letter, and a full draw of 100 letters consumes about 123 bytes. Long runs of rejection fade geometrically: each rejection has probability 18.75%, so 3 in a row happen about once in 152 letters and 10 in a row about once in 18.6 million.
| Pool | Size | Bits kept | Masked range | Rejected values | Acceptance | Bytes per letter |
|---|---|---|---|---|---|---|
| English alphabet | 26 | 5 | 0–31 | 26–31 (6) | 81.25% | 1.23 |
| Vowels | 5 | 3 | 0–7 | 5–7 (3) | 62.50% | 1.60 |
| Consonants | 21 | 5 | 0–31 | 21–31 (11) | 65.63% | 1.52 |
| Greek alphabet | 24 | 5 | 0–31 | 24–31 (8) | 75.00% | 1.33 |
| Both cases | 52 | 6 | 0–63 | 52–63 (12) | 81.25% | 1.23 |
Across every pool size this site can build — single-case pools of 2 to 26 letters, their mixed-case doubles up to the 52-entry pool, and the smaller Greek pools, each probed through the engine at build time with include lists of every length — the lowest acceptance rate is 53.13%, at pools of 17 and 34, where a letter costs 1.88 bytes on average. A pool of one letter draws no byte at all, so it has no rate. Never more than half the attempts wasted, fewer than two bytes per letter: the bound the methodology page states. Draws without repeats run the same sampler over a shrinking span; the no-repeats guide follows that through.
When rejection never happens
Two kinds of pool make the loop trivial. If the pool size is a power of two, the masked range equals the pool: nothing can land beyond it, acceptance is 100.00%, and every byte yields a letter on the first attempt. An include list of 4 letters is such a pool — 2 bits, mask 3 — and the byte 255, every one of its 8 bits set, masks straight to 3 and draws D. The other extreme is a pool of one letter. With nothing to choose, the engine draws no byte at all: 3 letters from an include list containing only Q come back as QQQ without consuming any randomness — the build asserts it with a byte source that throws if called.
Reading the result
Every result reports the exact pool size the draw came from, which is all you need to reconstruct what the sampler did: a pool of 26 means 5 bits kept and values 26–31 rejected; a pool of 5 means 3 bits and values 5–7 rejected. How a pool reaches its size is the subject of how letter pools are built. Two misreadings are worth heading off: "both cases" is a 52-entry pool, not an uppercase draw with a coin flip after it; and repeats or short streaks in your results are not a sign that rejection failed — they are what exact uniformity looks like on a small sample, as uniform randomness and streaks works through. Rejection sampling fixes the mapping from bytes to positions and leaves randomness exactly as streaky as it is. More on the guides index.
Frequently asked questions
Why does the engine mask before rejecting instead of rejecting the raw byte?
Both would be exactly uniform; masking keeps the waste small. Rejecting every raw byte at or beyond 26 would accept only 26 of 256 byte values — 10.16% — and a letter would take 9.85 bytes on average. Keeping the low 5 bits first shrinks the comparison to 32 values, so 26 of them are accepted (81.25%) and a letter takes 1.23 bytes. The shrink is legitimate because 32 divides 256 exactly, which the question on masking below covers.
What is being rejected — a letter?
No. What is rejected is a masked number that points past the end of the pool, before any letter is chosen. For 26 letters the engine keeps 5 bits, giving 0 to 31; the 6 values from 26 to 31 name no letter, so they are discarded and a fresh byte is drawn.
How many random bytes does one letter take?
For the 26-letter alphabet each attempt is accepted with probability 26/32 (81.25%), so a letter takes 1.23 bytes on average; vowels take 1.60, because a 5-letter pool fills only 5 of 8 masked values. No pool this site can build needs more than 1.88 bytes per letter on average.
Could the engine get stuck rejecting bytes forever?
Each attempt is independent and succeeds with probability above one half, so runs of rejection shrink geometrically: for the English alphabet, 3 in a row happen about once in 152 letters and 10 in a row about once in 18.6 million. The loop needs no counter.
Why 5 bits and not 6 for 26 letters?
Because 5 is the fewest bits that reach every position: 4 bits cover only 0 to 15 and could never name Q through Z. Any wider mask would also be exactly uniform but would reject more. With 6 bits the masked range is 0 to 63, only 26 of 64 values are accepted (40.63%), and a letter takes 2.46 bytes instead of 1.23. Choosing the minimal width is what keeps acceptance above one half for every pool.
Does masking itself introduce any bias?
No. Keeping the low 5 bits is the same as taking the byte modulo 32, and 32 divides 256 exactly — 256 = 8 × 32 — so each masked value from 0 to 31 is produced by exactly 8 of the 256 bytes. A remainder is even only when the divisor divides 256; every power of two up to 256 does and 26 does not. Masking is the even step; rejection removes the unevenness that remains between 32 and 26.
Uniform means uniform: rejection sampling is the reason every letter in the pool carries exactly the same probability — never weighted by English usage. Draws happen locally in your browser and are never transmitted. Details on the methodology page.