Methodology
The algorithm behind every value on this site, and the test results that verify it. Of the fifteen generators we audited on the first page of Google, none publishes either.
Where the randomness comes from
Every value is produced from crypto.getRandomValues(),
the browser's interface to the operating system's cryptographic random number
generator. On Linux that is the getrandom syscall,
on Windows BCryptGenRandom, on Apple platforms
SecRandomCopyBytes. All three draw from a kernel
pool seeded from hardware sources.
There is no fallback. If a browser cannot provide a secure source, the tool shows an error and refuses to generate. A silent downgrade to a weaker source is the worst possible failure for a tool people use to make real secrets, and it is exactly what several generators do.
The Web Crypto specification refuses a single request larger than 65,536 bytes, so bulk generation chunks around that limit. It also draws into a pooled buffer rather than calling the API once per character, which is why ten thousand strings take milliseconds rather than seconds. The values are identical either way; only the number of syscalls changes.
Removing modulo bias
Turning random bits into characters is where most generators go wrong. The obvious approach is to take the remainder of a random number divided by the alphabet size. It does not divide evenly, so some characters come up more often than others.
Concretely, mapping a single byte onto a 62-character alphabet: 256 = 4 x 62 + 8. Values 0 to 247 cover the alphabet four times each. Values 248 to 255 cover the first eight characters a fifth time. Those eight are drawn with probability 5/256 and the other fifty-four with 4/256, so they are 1.25 times more likely. Over a sixteen-character string that is 4.418 bits of min-entropy lost against the 95.267 the tool would claim.
We remove it with rejection sampling. Draws are taken as 32-bit values and compared against the largest exact multiple of the alphabet size:
limit = Math.floor(2 ** 32 / n) * n;
do { v = pool.next(); } while (v >= limit);
index = v % n; Values at or above the limit are discarded rather than folded in, so every accepted value maps onto exactly the same number of preimages. The cost is negligible: at 62 characters, exactly four values out of 2^32 are rejected, so a redraw happens about once per billion draws.
Character classes without positional bias
"Must contain at least one uppercase, one digit and one symbol" is usually implemented by generating a string and then overwriting fixed positions. That leaves a visible positional pattern. Shuffling afterwards removes the pattern only if the shuffle is itself unbiased, which the common naive version is not, and even a perfect shuffle leaves some valid strings more likely than others.
This tool uses whole-string rejection instead: it generates a complete candidate, tests it against every required class, and discards the whole thing on failure. Every valid string is produced with exactly equal probability, by construction. For an eight-character password over all 94 printable characters with four required classes, the acceptance rate means about 2.17 attempts per output. At sixteen characters it falls to 1.215.
Where a shuffle is needed the implementation is Fisher-Yates in its downward form, with indices from the same unbiased sampler. The naive variant that swaps each position with a random index over the whole range has n^n execution paths mapping onto n! permutations, and n! never divides n^n for n greater than 2, so some orderings must come up more often.
Entropy, reported honestly
The figure every generator prints is length multiplied by the log base two of the alphabet size. That is correct only when each character is independent and uniform. Requiring a character class, excluding look-alikes, or forbidding repeats all make the real entropy lower than that formula gives.
This tool computes the true value: the entropy of a uniform choice over the set of valid strings is the log base two of how many valid strings there are, so it counts them by inclusion-exclusion over the required-class partition. The same expression handles the no-repeat case using falling factorials. Below length 256 the count is computed exactly with arbitrary precision integers, because that is where floating-point cancellation would bite; above it the acceptance probability is so close to one that double precision is exact enough.
When a constraint costs entropy, the display shows both the real figure and the unconstrained one, so the cost is visible rather than hidden.
Test results
Generated 25,000 strings of 20 characters, which is 500,000 characters over an alphabet of 62. A test fails below p = 0.001. These numbers are produced by a committed script and regenerated on every release.
Our sampler
| Test | Statistic | p | Result |
|---|---|---|---|
| Character frequency | 55.55 on 61 df | 6.73e-1 | pass |
| Positional bias | 1212.68 on 1220 df | 5.54e-1 | pass |
| Adjacent repeats | z = 0.469 | 6.39e-1 | pass |
| Serial pairs | 3666.18 on 3843 df | 9.79e-1 | pass |
The control, which must fail
A test suite that passes everything proves nothing. So the identical
battery is run against a deliberately biased implementation: the naive
byte % 62 mapping. If it passed, the
clean result above would mean nothing either.
| Test | Statistic | p | Detected? |
|---|---|---|---|
| Character frequency | 3345.40 on 61 df | < 1e-300 | caught |
| Positional bias | 4549.29 on 1220 df | < 1e-300 | caught |
| Adjacent repeats | z = 0.446 | 6.56e-1 | not caught |
| Serial pairs | 10069.33 on 3843 df | < 1e-300 | caught |
The adjacent-repeat test does not catch modulo bias, and that is expected rather than a defect. It measures how often two neighbouring characters are equal, which stays close to 1 in 62 even when the marginal frequencies are skewed. It is included because it catches a different failure, a generator whose successive draws are correlated.
The statistics implementation, checked against published values
The chi-square and normal tail probabilities are computed from an incomplete gamma function rather than a lookup table, so that implementation is itself verified against known critical values before any result above is trusted.
| Check | Computed | Published | |
|---|---|---|---|
| chi2 crit df=1 at p=0.05 | 0.050000 | 0.05 | ok |
| chi2 crit df=10 at p=0.05 | 0.050000 | 0.05 | ok |
| chi2 crit df=61 at p=0.05 | 0.050003 | 0.05 | ok |
| chi2 crit df=61 at p=0.01 | 0.010001 | 0.01 | ok |
| normal two-sided at z=1.959964 | 0.050000 | 0.05 | ok |
| normal two-sided at z=2.575829 | 0.010000 | 0.01 | ok |
Speed
| Batch | Total | Per string |
|---|---|---|
| 100 x 32 characters | 0.26 ms | 2.63 us |
| 1,000 x 32 characters | 5.6 ms | 5.60 us |
| 10,000 x 32 characters | 16.84 ms | 1.68 us |
| 10,000 x 64 characters | 25.82 ms | 2.58 us |
Verified. Our sampler passes every test and the biased control is detected. Both halves of that sentence matter. Generated 2026-09-13.
What we do not claim
We do not claim better entropy than a hardware source. RANDOM.ORG samples atmospheric noise and that is genuinely a different and in some senses superior source; saying otherwise would be easy to disprove. What we claim instead is that our output is locally generated, that the mapping from bits to characters is provably uniform, and that both are auditable.
We also do not claim that every other generator is broken. Of fifteen
audited, five use Math.random() in
generator logic, one uses a secure source with a biased mapping, two are
textbook-correct, and seven generate server-side with legitimate sources.
Correct rejection sampling is table stakes, not a differentiator. What none
of them does is publish the algorithm and the test results.
Nothing you generate is transmitted, logged or stored. You do not have to take that on trust either: open your browser devtools, switch to the Network tab, and press Generate. Nothing appears, because there is no server to send it to.