Skip to content
RandoKit
EN

Randomness

What's really happening when a computer picks a random number

A plain-language look at seeds, periods, entropy, modulo bias, and rejection sampling — and how to check whether a generator is behaving fairly.

8 min readUpdated

"Random" is doing a lot of work in the phrase "random number generator." Almost nothing running on a computer is random in the way a radioactive atom's decay is random. Most number generators are deterministic machines that produce output which merely looks unpredictable to an observer who doesn't know their internal state. Understanding that distinction — and the handful of engineering tricks built on top of it — explains why some generators are fine for a game animation and completely wrong for a prize drawing.

True randomness vs. algorithmic randomness

There are two broad families. True randomness comes from a physical process nobody can fully predict even in principle, given current physics: thermal noise in a resistor, timing jitter between keystrokes, atmospheric radio static. Algorithmic (pseudo-) randomness comes from a formula that takes a starting number, called a seed, and grinds out a long sequence of outputs. Give the formula the same seed twice and you get the exact same sequence twice — that's not a flaw, it's how the math works. The output can pass every statistical test for randomness and still be 100% reproducible if you know the seed and the formula.

Nearly every "random" number your computer produces is the second kind, at least at some layer. The interesting engineering question is not "is this random?" but "how hard is it for someone to reconstruct the seed or internal state, and does that matter for what I'm using it for?"

Seeds and deterministic state

A pseudorandom number generator (PRNG) is a small state machine. It starts with a seed, applies a transformation to produce an output and a new internal state, then repeats. The classic teaching example is the linear congruential generator (LCG), which has been used since the 1950s and still shows up in textbooks because it's easy to compute by hand.

let state = 7;          // the seed
const a = 5, c = 3, m = 16;

function next() {
  state = (a * state + c) % m;
  return state;
}

// next() calls produce: 6, 1, 8, 11, 10, 5, 12, 15, ...
A tiny LCG: state = (a × state + c) mod m

Notice that once you know

a
,
c
,
m
, and any single output, you can compute every past and future value. That's true of essentially all classic PRNGs, including the one behind
Math.random()
in most JavaScript engines. It's a feature for reproducible simulations and a serious problem for anything a person could contest — see Math.random vs crypto.getRandomValues for where that line matters in practice.

Period and repetition

Because the state is a finite number (here, one of 16 possible values, 0 through 15), the sequence has to repeat eventually — this repeat length is called the period. A bad LCG can have a period of just a few thousand values, meaning after that many draws the exact same sequence starts over. Modern general-purpose PRNGs (like xorshift128+ or PCG, used inside browsers and language runtimes) have periods so long — often 2^128 or higher — that repetition is not a practical concern for everyday use. Period length says nothing about unpredictability, though: a generator can have an astronomically long period and still be trivially predictable if you can observe its state.

Where operating systems get real entropy

To seed something that resists prediction, operating systems harvest entropy — unpredictable physical measurements — from multiple sources: interrupt timing jitter (the exact nanosecond a key was pressed or a network packet arrived), hardware random number generators built into modern CPUs (Intel's RDRAND, ARM's equivalent), disk seek timing on older systems, and mouse movement. The OS mixes these measurements into a pool and uses that pool to seed a cryptographically secure PRNG (CSPRNG), which then stretches that entropy into as many random-looking bytes as any program asks for. This is why a freshly booted virtual machine with no physical hardware entropy sources sometimes struggles to generate secure keys quickly — there's less physical noise to draw from.

How a browser exposes randomness to a web page

A web page can't read the OS entropy pool directly, but it can ask for it indirectly through

crypto.getRandomValues()
, a browser API that fills a typed array with bytes derived from the OS's CSPRNG. This is the mechanism behind every RandoKit tool — the wheel of names, the random number generator, and the coin flip tool all pull raw bytes this way rather than relying on
Math.random()
. See how randomness works on this site for the implementation details specific to RandoKit.

What "uniform distribution" actually means

A generator is uniform over a range if every value in that range has exactly equal probability of appearing on any given draw — no value is favored, no value is starved. A fair six-sided die is uniform over {1, 2, 3, 4, 5, 6}: each face has exactly a 1-in-6 chance. Uniformity is a claim about the long-run frequency of outcomes, not about any individual draw looking "spread out" — five consecutive 6s from a genuinely uniform die are just as likely as any other specific sequence of five rolls.

Modulo bias: a worked example

A common way to turn a random byte into a random die roll is to take the byte's value modulo 6, then add 1. A byte has 256 possible values, 0 through 255. Here's the problem: 256 does not divide evenly by 6.

256 / 6 = 42 remainder 4

Values 0-251 (252 values) split evenly: 42 bytes map to each face.
Values 252-255 (4 leftover values) map to faces 1, 2, 3, 4 only:
  252 % 6 = 0  -> face 1
  253 % 6 = 1  -> face 2
  254 % 6 = 2  -> face 3
  255 % 6 = 3  -> face 4

Result: faces 1-4 each get 43 winning bytes out of 256.
        faces 5-6 each get only 42 winning bytes out of 256.
256 bytes mapped onto 6 die faces via modulo

That's a small skew — about a 2.3% relative excess on faces 1 through 4 — but it's real, systematic, and it compounds over many rolls. For a small range against a large byte range the bias is often negligible; for something like mapping a byte onto a range of 100 (256 / 100 = 2 remainder 56), the skew becomes large enough to matter, favoring the low values by a noticeable margin.

How rejection sampling removes the bias

The fix is to throw away the leftover values instead of wrapping them around. Compute the largest multiple of 6 that fits inside 256 (that's 252), and if a drawn byte is 252 or higher, discard it and draw again. Every byte that survives is one of exactly 252 possible values, split into six perfectly equal groups of 42 — no skew, at the cost of occasionally needing an extra draw (here, about 1.6% of the time). This is the approach used by any careful implementation of an integer picker, including RandoKit's dice roller and random number generator, and it's covered with a code example in Math.random vs crypto.getRandomValues.

Sanity-checking a generator yourself

You don't need special tools to spot a badly biased generator — you need enough rolls and a calculator. Roll a six-sided die 600 times and record the count for each face. If the generator is fair, you'd expect roughly 100 hits per face, and normal statistical variation means individual faces will land somewhere around 85-115 without anything being wrong. What should worry you is a face consistently landing far outside that band across repeated batches of 600, or one face never coming up at all.

  1. Decide on a sample size large enough to smooth out noise — a few hundred draws minimum.
  2. Record the observed count for each possible outcome.
  3. Compare each observed count to the expected count (total draws ÷ number of outcomes).
  4. Repeat the whole batch once or twice — a real bias shows up consistently, a fluke doesn't.

The takeaway

Every number generator, secure or not, is built from the same handful of ideas: a seed, a deterministic or physically-driven state update, and a mapping from raw output onto the range you actually need. The differences that matter are how hard the seed and state are to predict, and whether the mapping onto your target range introduces bias. If you're picking a name from a wheel, rolling dice for a game night, or drawing a prize winner, the safest default is a generator built on

crypto.getRandomValues()
with proper rejection sampling — read Math.random vs crypto.getRandomValues next to see exactly where that line gets drawn in real code.

Tools used in this guide