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.

The one decision behind every random callThe function you type picks the machine. A statistical PRNG is safe only when no secret depends on the output. A CSPRNG is safe for tokens, keys, and salts. Route a secret through the predictable branch and the sequence is recoverable.

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.

The work to predict the next outputa blink vs never

624 outputs

Statistical PRNG (Mersenne Twister)

out of reach

~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
The work to predict the next output (effort to recover the generator)
Optioneffort 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.

The three layers under every random callLayer 1 is hardware noise: RDSEED, interrupt and device timing, clock jitter. Layer 2 is the kernel: an input pool feeds a ChaCha20 stream that getrandom() and /dev/urandom expose. Layer 3 is the language API. crypto.getRandomValues() and os.urandom() read the kernel every call; Math.random() is seeded once, then runs alone.

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.

seed.c · c
#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.

  1. Hardware entropyRDSEED / RDRAND, interrupt timing, and device jitter feed in continuously. This is the only place unpredictability enters.
  2. Input poolThe kernel accumulates and mixes the noise into an entropy pool, smoothing out any single weak source.
  3. ChaCha20 streamThe pool seeds a ChaCha20 stream cipher. Its keystream is the fast, unpredictable output that scales to gigabytes per second.
  4. getrandom()getrandom(2) and /dev/urandom expose the stream. getrandom blocks only until the pool is first seeded after boot, then never again.
  5. 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.
The kernel CSPRNG, from hardware noise to the bytes you read. The numbered notes below carry the detail.

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.

js
// 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));

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.

Which call maps to which machine, and whether it is safe for secrets
The call you typeAlgorithm underneathPredictable from state?Safe for secrets?
Math.random()xorshift128+ (V8)YesNo
random.random() (Python)Mersenne TwisterYesNo
math/rand (Go)seeded PRNGYesNo
crypto.getRandomValues()kernel CSPRNG (ChaCha20)NoYes
secrets / os.urandom (Python)kernel CSPRNGNoYes
crypto/rand (Go)kernel CSPRNGNoYes

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.

Von Neumann's middle-square, stepped by hand

Starting from seed 1329, the middle-square method collapses to zero in 7 steps:

  1. 1329 1329² = 01766241, middle four = 7662
  2. 7662 7662² = 58706244, middle four = 7062
  3. 7062 7062² = 49871844, middle four = 8718
  4. 8718 8718² = 76003524, middle four = 0035
  5. 0035 0035² = 00001225, middle four = 0012
  6. 0012 0012² = 00000144, middle four = 0001
  7. 0001 0001² = 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.

Pick a 4-digit seed and step it. Square the value, pad to eight digits, take the middle four; that is the next value. Watch it collapse. With JavaScript off, the default seed's full sequence is listed below.

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.

xorshift128plus.c · c
// 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.

Seed-replay: predict the next token, then fail to

Your session tokens

  1. ae2cb148
  2. 335ea407
  3. 924711ba
  4. b644f5d1predicted

Attacker's view

State recovered

Your next token will be

f949b4fc

The 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.

Generate tokens from a statistical PRNG and the attacker recovers the state and prints your next token in advance. Switch the source to a CSPRNG and the attacker has no recurrence to solve. With JavaScript off, an illustrative attack-in-progress scene is shown.

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

Ayush Makwana

Software Engineer, Atyantik Technologies

Ayush Makwana is a Software Engineer at Atyantik Technologies, a software product studio building web platforms and integrated systems since 2015. His writing digs one layer below the framework, into how the machine and the browser actually behave.

More from Ayush MakwanaApplication securityHire Node.js developers

Keep reading