You typed one of two functions
Math.random()
predictable
A deterministic formula. Rewind and the same bytes come back.
crypto.getRandomValues()
unpredictable
Kernel entropy. Rewind and fresh bytes come out, never the old ones.
One is a formula you can replay. One is entropy you cannot. That single difference decides every random call you will ever write.
How computers generate random numbers, one decision at a time
Two machines answer every random() call: a fast, fully predictable statistical PRNG, and an unpredictable kernel CSPRNG seeded from real hardware entropy. One question decides which one you need, and choosing wrong on the security side is how session tokens get predicted.
Two machines, one question: predictable or unpredictable?#
Most explainers open by asking whether a computer can be "truly" random. That framing sends you down a physics rabbit hole about lava lamps and radioactive decay. It answers nothing you type into an editor. Instead, start where the decision actually lives. The honest answer to how computers generate random numbers begins with one split: are you calling a machine you can replay, or a machine you cannot?
Everything else follows from that split. A statistical PRNG is a formula. Feed it the same starting state and it produces the same stream, every time, on every machine. Because it is a formula, it is fast and reproducible. A CSPRNG is seeded from real-world noise and is designed so that watching its output tells you nothing about its next value. So the two are not "better" and "worse". They are two tools for two different jobs, and the whole skill is knowing which job you have.
Which machine runs when you call random()?#
The function name is the fork in the road. A plain random()-style call reaches the statistical PRNG. A crypto-prefixed call reaches the kernel CSPRNG. After that first branch, only one more question matters, and it decides whether a predictable stream is harmless or dangerous.
How much does the wrong pick actually cost?#
The gap between the two machines is not a matter of degree. It is the difference between a puzzle a laptop solves in a blink and a puzzle no computer will ever solve. For example, the Mersenne Twister behind Python's random is fully recovered from 624 consecutive 32-bit outputs. After that, every future value is known. A kernel CSPRNG offers no such shortcut.
624 outputs
Statistical PRNG (Mersenne Twister)
~2^256 ops
Kernel CSPRNG (ChaCha20)
A Mersenne Twister is fully reconstructed from 624 observed outputs. Predicting a CSPRNG would take on the order of 2^256 operations, which no present or foreseeable computer performs.
Show data table
| Option | effort to recover the generator |
|---|---|
| Statistical PRNG (Mersenne Twister) | 624 outputs |
| Kernel CSPRNG (ChaCha20) | ~2^256 ops |
How computers generate random numbers: three layers under every call#
Underneath that one decision sits a short stack, and it is the same on every modern system. Three layers carry a random value from physical noise to the function you type. Moreover, every named function you will ever call slots into exactly one of these layers. Once you can locate a call in the stack, its behaviour stops being mysterious.
The stack: entropy source to kernel CSPRNG to language API#
The hardware makes noise. Then the kernel turns that noise into a fast, unpredictable stream. Finally the language wraps it in the call you type. First the physical layer, second the kernel layer, third the API layer. Read the map once and every function below has a home.
Layer 1: where the unpredictability is born#
Unpredictability has to come from somewhere physical, because a formula alone cannot invent it. Therefore the kernel harvests noise the hardware produces as a side effect of running. Modern CPUs expose the RDSEED and RDRAND instructions, which sample on-chip thermal noise. Meanwhile the kernel also times interrupts, disk and network events, and scheduling jitter, all of which are hard to predict from outside the machine. It mixes these sources into an entropy pool. You never read this layer directly. You read the kernel wrapper above it.
#include <sys/random.h>
#include <stdint.h>
uint8_t key[32];
// getrandom() reads the kernel CSPRNG. It blocks only once, until the pool is
// seeded the first time after boot, then never again.
if (getrandom(key, sizeof key, 0) != sizeof key) abort();
// You never touch the hardware directly. Under the pool, the kernel mixed in
// RDSEED / RDRAND from the CPU, plus interrupt and device timing, plus
// scheduling jitter. That noise is the raw unpredictability everything above
// is built on. Layer 2: the kernel CSPRNG (ChaCha20, /dev/urandom, getrandom)#
Raw hardware noise is slow and uneven, so the kernel does not hand it to you directly. Instead it uses that noise to seed a cryptographic stream cipher, ChaCha20 on Linux, and reads the cipher's output as random bytes. Because ChaCha20 is fast, the kernel can produce gigabytes per second while staying unpredictable. Periodically it reseeds from fresh pool entropy, so even a leaked internal state does not compromise output forever. The anatomy below shows how the pieces connect.
- Hardware entropyRDSEED / RDRAND, interrupt timing, and device jitter feed in continuously. This is the only place unpredictability enters.
- Input poolThe kernel accumulates and mixes the noise into an entropy pool, smoothing out any single weak source.
- ChaCha20 streamThe pool seeds a ChaCha20 stream cipher. Its keystream is the fast, unpredictable output that scales to gigabytes per second.
- getrandom()getrandom(2) and /dev/urandom expose the stream. getrandom blocks only until the pool is first seeded after boot, then never again.
- Periodic reseedFresh pool entropy is folded back into the cipher on an interval, so a one-time state leak does not doom all future output.
Layer 3: the language API you actually call#
The top layer is the only one you touch, and every language draws the same line through it. One family of calls is the statistical PRNG. The other family reads the kernel CSPRNG. Furthermore the two live side by side in the standard library, so picking the right one is a matter of habit, not extra dependencies. Here is the same choice in three languages.
// Predictable: a statistical PRNG. Good for a shuffle or a jitter delay.
// Never for anything a secret depends on.
const wobble = Math.random();
// Unpredictable: the kernel CSPRNG. Use this for tokens, keys, and salts.
const token = crypto.getRandomValues(new Uint8Array(32)); import random, secrets, os
# Predictable: the Mersenne Twister. Reproducible from its internal state.
sample = random.random()
# Unpredictable: both route to the OS CSPRNG.
token = secrets.token_hex(32) # convenience wrapper
raw = os.urandom(32) # the raw kernel bytes import (
mrand "math/rand"
crand "crypto/rand"
)
// Predictable: math/rand is deterministic from its seed.
_ = mrand.Float64()
// Unpredictable: crypto/rand reads the kernel CSPRNG.
buf := make([]byte, 32)
_, _ = crand.Read(buf) The pattern is identical across ecosystems, so the lookup table is worth keeping close. For each call it names the algorithm, whether the output is predictable from state, and whether it is safe for a secret.
| The call you type | Algorithm underneath | Predictable from state? | Safe for secrets? |
|---|---|---|---|
| Math.random() | Algorithm underneathxorshift128+ (V8) | Predictable from state?Yes | Safe for secrets?No |
| random.random() (Python) | Algorithm underneathMersenne Twister | Predictable from state?Yes | Safe for secrets?No |
| math/rand (Go) | Algorithm underneathseeded PRNG | Predictable from state?Yes | Safe for secrets?No |
| crypto.getRandomValues() | Algorithm underneathkernel CSPRNG (ChaCha20) | Predictable from state?No | Safe for secrets?Yes |
| secrets / os.urandom (Python) | Algorithm underneathkernel CSPRNG | Predictable from state?No | Safe for secrets?Yes |
| crypto/rand (Go) | Algorithm underneathkernel CSPRNG | Predictable from state?No | Safe for secrets?Yes |
Where the bits come from: a worked example#
Abstract layers are easy to nod along to and easy to forget. So it helps to run a real generator by hand and watch it break. The failure of the earliest software PRNG is the oldest lesson in how randomness works in computers, and it still explains why the modern stack looks the way it does.
Von Neumann's middle-square, stepped with real digits#
In 1946 John von Neumann proposed the middle-square method. Square the current number, pad it to eight digits, and take the middle four as the next number. It is charmingly simple. However it degenerates fast, often collapsing to zero or falling into a tiny repeating cycle. Pick a seed below and step it yourself. Watch a healthy-looking number die within a handful of iterations.
The sequence so far
Starting from seed 1329, the middle-square method collapses to zero in 7 steps:
-
13291329² = 01766241, middle four =7662 -
76627662² = 58706244, middle four =7062 -
70627062² = 49871844, middle four =8718 -
87188718² = 76003524, middle four =0035 -
00350035² = 00001225, middle four =0012 -
00120012² = 00000144, middle four =0001 -
00010001² = 00000001, middle four =0000
Once the value reaches 0000 it stays there forever. Short
seeds and near-zero values are the collapse the method never escaped.
That collapse is the whole reason nobody ships a naive recurrence today. A generator whose period is a few dozen values, or whose future is a single fixed point, leaks its state and repeats itself. Consequently every serious PRNG since has been designed for an enormous period and a well-mixed state. Yet a large period alone still does not make a generator safe for secrets, which is the next trap.
What V8 really runs: xorshift128+ with real constants#
Fast-forward to the generator in your browser. When you call Math.random() in Chrome or Node, V8 runs xorshift128+. It has a huge period and passes strong statistical tests, so its output looks random to any casual inspection. Still, it is a pure formula over 128 bits of state. Given those bits, every future output is fixed, and the state is recoverable from a handful of outputs.
// V8's Math.random() is xorshift128+ (see the V8 blog below). Two 64-bit words
// of state, advanced with shifts and XORs. Nothing is mixed in after seeding.
uint64_t s1 = state0;
uint64_t s0 = state1;
state0 = s0;
s1 ^= s1 << 23;
s1 ^= s1 >> 17;
s1 ^= s0;
s1 ^= s0 >> 26;
state1 = s1;
// The next double is derived from (state0 + state1). Given those two words,
// every future output is already fixed. There is no secret here to protect. When predictable becomes recoverable: the seed-replay attack#
This is the moment how computers generate random numbers stops being trivia and starts costing money. A statistical PRNG hands its state to anyone who watches enough output. If your session tokens come from that stream, an attacker who collects a few tokens can compute your users' next token before it is issued. Below, generate mock tokens from a seeded statistical PRNG and watch the attacker pane recover the state and predict your next value. Then flip the source to a CSPRNG and watch the same attack die.
Your session tokens
ae2cb148335ea407924711bab644f5d1predicted
Attacker's view
State recovered
Your next token will be
f949b4fcThe output is the generator's own state, and the algorithm is public, so one observed token hands over the entire future stream.
The attacker has already recovered the generator and predicted your next token. Generate it and watch it get stolen.
The demo uses a simple linear congruential generator so the recovery is instant and obvious. Real generators such as the Mersenne Twister need more observed outputs, yet the outcome is the same in kind. Because the algorithm is public and only the state is hidden, enough output reveals the state, and the state reveals the future. Therefore predictable means recoverable, and recoverable means a token you thought was secret is not.
When not to reach for each generator#
The security spine cuts both ways, so the honest guidance is not "always use the CSPRNG". Reaching for the wrong tool wastes speed in one direction and safety in the other. Match the generator to the job.
The one question, one more time#
Strip away the layers and one question remains. Does a secret depend on this output? If yes, call the CSPRNG: crypto.getRandomValues(), secrets, os.urandom(), or crypto/rand. If no, a statistical PRNG is faster and reproducible, and that is exactly what you want. That is how computers generate random numbers, reduced to one decision you can make without thinking about lava lamps.
This sits next to the rest of the platform-level work we write about. If you build crypto on the edge, our walkthrough of Web Push VAPID from scratch on Web Crypto uses the same crypto.subtle family the CSPRNG lives beside. Because Math.random() is a language feature, our guide to modern ECMAScript for 2026 is the companion read on where these APIs are heading. And since the kernel CSPRNG runs wherever your code runs, rendering a React app on Cloudflare covers the Workers runtime where crypto.getRandomValues() is the only random you get.
Getting randomness wrong is quiet until it is not, and it usually surfaces as a security incident rather than a bug report. If you want a second set of hands on token generation, key handling, or an audit of where predictable randomness leaked into something sensitive, we are glad to help. No pressure and no lock-in.
Talk to our software engineering team