Back to Blog

How random numbers work in computers

When you call Math.random() in JavaScript (or the equivalent in your preferred language), you get back a decimal number like 0.7291 or 0.1842. These numbers are uniformly distributed, meaning every value is equally likely. You're just as likely to get 0.1 as 0.9, and over many calls, the values spread evenly across the range.

But these numbers aren't truly random. They're the result of a mathematical formula that produces numbers which look random but are completely predictable if you know the starting point.

These are called pseudorandom number generators (PRNGs). The "pseudo" is important: they're not truly random, just good enough to fool most statistical tests.

Math.random() Explorer
Click Generate to see distribution
Math.random() // returns [0, 1)

Generate some numbers and watch the distribution. With enough samples, each bucket should contain roughly the same count. That's what "uniform" looks like.

The problem with true randomness

Computers are deterministic machines. Given the same inputs, they produce the same outputs. That's the opposite of randomness. To get true randomness, you'd need to measure something physical: radioactive decay, thermal noise, or the unpredictable timing of your inputs.

Entropy from Movement
Click and drag to collect entropy
Entropy collected0 samples
Generated bytes
Move around to generate

Your movement coordinates and timing are unpredictable. Each position depends on muscle movements, reaction time, and small decisions you're not even aware of. Operating systems collect this kind of physical measurement to seed their random number generators.

But physical randomness is slow to collect. When a game needs to generate a procedural world or a simulation needs millions of random samples, we need something faster.

The first PRNG: middle-square method

In 1946, John von Neumann invented one of the first computer-based random number generators:

  1. Start with a number (the seed)
  2. Square it
  3. Take the middle digits as your next "random" number
  4. Repeat
The Middle-Square Method
1234015227565227
History:

Try seed 0000 or 2500 to see cycles form quickly. The middle-square method has a fatal flaw: sequences can get stuck in repeating patterns.

Von Neumann knew about these problems. He famously said: "Anyone who considers arithmetical methods of producing random digits is, of course, in a state of sin."

What would a better algorithm look like?

The middle-square method fails because it can get trapped in short cycles. What properties would we need to avoid this?

  1. Longer cycles: The sequence should visit many values before repeating
  2. No traps: There shouldn't be "dead end" states that collapse to zero
  3. Uniform coverage: All possible values should appear roughly equally often

These requirements led researchers to a different approach: instead of squaring and extracting, what if we used operations that naturally cycle through all possible values?

Linear congruential generators

The most common PRNG algorithm for decades was the Linear Congruential Generator (LCG):

next = (a * current + c) mod m

Take the current number, multiply by a, add c, then wrap it into range using mod m. The result becomes the next "random" number, and also the input for generating the one after that.

Every PRNG needs a starting value called the seed, the very first "current" value. From the seed, the algorithm generates a sequence of numbers. Three parameters control how that sequence behaves:

  • a (multiplier): scales the previous value
  • c (increment): adds an offset
  • m (modulus): wraps the result back into range (the mod operation gives the remainder after division, so 17 mod 5 = 2)
Linear Congruential Generator
current = seed = 7 → click Next
7

Click Next to generate values. Try the "glibc" preset to see the parameters used by the GNU C Library (the standard library on most Linux systems): a=1103515245, c=12345, m=2³¹. Change the seed and notice how the entire sequence changes. Same algorithm, different starting point, completely different output.

Seeds and reproducibility

We introduced the seed as the starting value. The same seed always produces the same sequence.

Seeds and Reproducibility
Seed A:
Click Generate
Seed B:
Click Generate

Set both seeds to the same value and generate. The sequences match perfectly. Change one seed and they diverge completely.

This is useful: in games, you can save the world generation seed and recreate the exact same world later. But it's also a security risk. If an attacker can guess your seed, they can predict all future "random" numbers.

The period problem

Every PRNG eventually repeats. After a certain number of values, you'll see the same sequence start over. This length is called the period.

Why does this matter? Imagine you're running a Monte Carlo simulation with 10 million random samples, but your PRNG's period is only 65,536. After that point, you're just reusing the same "random" numbers. Your simulation results become meaningless because you're not exploring the space, you're cycling through it.

PRNG Period
Period: 0 (max possible: 7)

The highlighted numbers show where the cycle starts repeating. Try the presets to see different period lengths. "Full period" achieves the maximum possible period for its modulus.

For an LCG with modulus m, the maximum possible period is m. But you only achieve the full period with carefully chosen parameters. Bad choices lead to much shorter cycles.

Modern PRNGs like the Mersenne Twister have periods of 2^19937 - 1. That's a number with about 6,000 digits.

Visualizing PRNG quality

Period length isn't the only measure of quality. Even with a long period, a PRNG can have subtle patterns that make it unsuitable for certain applications.

Good PRNGs produce numbers that appear uniformly distributed with no visible patterns. Bad ones show structure.

Visualizing PRNG Quality
glibc LCG (good parameters)
Small modulus LCG (m=1024)

Click "Regenerate" to see different seeds. The top plot uses glibc's LCG (a=1103515245, c=12345, m=2³¹), which fills the space uniformly. The bottom uses a small modulus (1024). With fewer possible values, you can see diagonal striping where points cluster along lines. This lattice structure is why parameter choice matters so much for LCGs.

Modern algorithms: xorshift

George Marsaglia introduced the xorshift family of generators in 2003. They're extremely fast and produce high-quality output. But why these specific operations?

What makes good bit mixing?

For a PRNG to produce quality output, it needs to "mix" bits thoroughly. Each output bit should depend on many input bits in complex ways. We need operations that:

  1. Are reversible: So we don't lose information and shorten our period
  2. Create avalanche: Small input changes cause large output changes
  3. Are fast: Just a few CPU instructions

XOR and bit shifting happen to satisfy all three. Let's see how they work.

What is XOR?

XOR (exclusive or) compares two bits and outputs 1 if they're different, 0 if they're the same.

XOR (Exclusive Or)
A:
B:
A^B:
1
1
0
1
0
1
0
0

Click any bit to toggle it. Notice how the result changes based on whether the bits match or differ.

What is bit shifting?

Shifting moves all bits left or right by some amount. Bits that "fall off" the edge are lost, and zeros fill in the empty positions.

Bit Shifting
Original:
0
0
1
1
0
1
0
0
= 52
<< 2:
1
1
0
1
0
0
0
0
= 208
Value:(0-255)

Left shift (<<) multiplies by powers of 2. Right shift (>>) divides. Try different values and shift amounts.

Putting it together: xorshift

The xorshift algorithm combines these operations. It XORs a number with shifted versions of itself, which mixes the bits thoroughly.

Xorshift Step-by-Step
1
0
1
1
0
1
1
1
= 183
Start10110111(183)
Seed:(1-255)

Step through the algorithm to see how each operation transforms the bits. After all steps, click "Apply & Continue" to use the result as the next seed.

The real xorshift uses 32 or 64 bits with carefully chosen shift amounts. Modern JavaScript engines use xorshift128+, a variant that passes all standard statistical tests.

From uniform to useful

PRNGs produce uniform values in the range [0, 1). These are continuous numbers (decimals like 0.7291, 0.0003, or 0.9999), not integers. The notation means "from 0 up to but not including 1." The square bracket means "inclusive" and the parenthesis means "exclusive."

Applications often need different distributions: integers, dice rolls, or shuffled arrays.

Converting to integers

How do we turn a decimal like 0.73 into a dice roll from 1-6? We can "stretch" the [0, 1) range to cover [0, n), then snap to whole numbers.

Step by step:

  1. Start with a value in [0, 1), say 0.73
  2. Multiply by your range size: 0.73 × 6 = 4.38
  3. Take the floor to get a whole number: floor(4.38) = 4
  4. Add 1 if you want 1-indexed results: 4 + 1 = 5
From [0, 1) to Integers
Uniform value:0.420
0123456
uniform: 0.420
× 6: 2.520
floor: 2 (add 1 for dice: 3)

Drag the slider to see how the continuous range maps to discrete buckets. Each bucket gets an equal slice of the [0, 1) range.

Dice and integers

To convert a uniform [0, 1) value to a dice roll:

const d6 = Math.floor(Math.random() * 6) + 1;
Dice Rolls
123456
Math.floor(Math.random() * 6) + 1

With one die, each outcome is equally likely. With multiple dice summed together, middle values become more common because there are more ways to achieve them.

Shuffling arrays

How do you randomly shuffle an array? A naive approach might be: for each position, swap it with a random other position.

Naive Shuffle (Biased)
Loading...

Step through the algorithm. Notice how j can be any position from 0 to n-1, including positions we haven't visited yet.

The correct algorithm is Fisher-Yates:

Fisher-Yates Shuffle
Loading...

Iterate backward, swapping each position with a random earlier position (including itself). Notice that j only picks from positions 0 to i, not the entire array.

Why naive shuffling is biased

With 3 cards, there are 3! = 3×2×1 = 6 possible orderings. But the naive "swap each with random" makes 3³ = 3×3×3 = 27 random choices. Since 27 doesn't divide evenly by 6, some orderings must be more likely than others.

Shuffle Distribution Comparison

Click the button to run 10,000 shuffles with each algorithm and compare the distributions.

With 3 cards, there are 6 possible orderings. Each should appear ~1,667 times if unbiased.

Run the simulation to see the bias. The naive shuffle consistently overrepresents some orderings and underrepresents others. Fisher-Yates produces a uniform distribution.

When Math.random() isn't enough

So far we've focused on statistical quality: uniform distribution, long periods, no patterns. For games, simulations, and visualizations, Math.random() is perfect.

But there's another dimension: predictability. For security applications, this matters more than statistical quality.

Remember that PRNGs are deterministic: the same seed produces the same sequence. If an attacker can observe a few outputs from your PRNG, they can often reverse-engineer the internal state and predict all future outputs.

A concrete attack: Imagine a website generates password reset tokens using Math.random(). An attacker creates an account, requests a reset token for themselves, and observes the "random" value. With enough samples, they can predict the token that will be generated for your password reset request and hijack your account.

Math.random() vs crypto
Math.random() (not for security)
Click generate
crypto.getRandomValues() (secure)
Click generate

For generating tokens, passwords, or encryption keys, use crypto.getRandomValues(). It draws from the operating system's CSPRNG (Cryptographically Secure Pseudo-Random Number Generator).

What makes a PRNG "cryptographically secure"?

A regular PRNG like xorshift is designed for speed and statistical quality. A CSPRNG adds a stronger requirement: even if you see the output, you can't predict future values or recover past ones.

Observing outputs doesn't let you deduce the internal state. Even if the state is somehow compromised, past outputs remain unpredictable. And the generator mixes in fresh entropy periodically, so the state keeps changing.

Where does the entropy come from?

Modern systems collect entropy from multiple sources. Intel and AMD CPUs have built-in random number generators (the RDRAND instruction) that measure thermal noise in the silicon. The exact timing of keyboard presses, mouse clicks, and network packets is unpredictable at the microsecond level. Even hard drive seek times vary based on physical factors.

The operating system maintains an "entropy pool" that continuously collects these measurements. Linux uses /dev/urandom, Windows uses CryptGenRandom, and browsers expose this through crypto.getRandomValues().

Math.random() is fast but predictable if you know the internal state (about 128 bits). crypto.getRandomValues() is slower but unpredictable even with full system knowledge, because it's continuously reseeded from hardware.

Summary

Computers can't generate true randomness, but they can produce sequences that are random enough for most purposes:

  • PRNG: An algorithm that produces deterministic but random-looking sequences
  • Seed: The starting value that determines the entire sequence
  • Period: How many values before the sequence repeats
  • LCG: Classic algorithm using multiply-add-modulo
  • Xorshift: Modern algorithm using XOR and bit shifts
  • CSPRNG: A PRNG designed so that observing outputs doesn't let you predict future values
  • Entropy pool: The OS-level collection of physical randomness from hardware and timing

Math.random() runs a deterministic formula. crypto.getRandomValues() reads from hardware.