Back to blog

I Tried to Predict Google's Random Number Generator So I Wouldn't Have to Study

Three hours before my Design and Analysis of Algorithms lab exam, I made a decision.

Three hours before my Design and Analysis of Algorithms lab exam, I made a decision.

I was not going to study all ten programs.

The format was simple. Walk into the lab, open Google's Random Number Generator widget, generate a number between 1 and 10, and execute whatever experiment that number mapped to. Most students would study all ten. I had a different plan: figure out which numbers were more likely to come up, study only those, and reclaim a few hours of my life.

My logic was flawless.

Why Computers Can't Be Truly Random

The first thing I confirmed is something most people don't think about: computers are incapable of randomness by design.

Every operation a computer performs is deterministic. Feed it the same inputs and it produces the same outputs, every time, without exception. There is no "choosing" happening — just arithmetic. So when you click Google's number generator and it shows you a 7, the machine didn't randomly pick 7. It computed 7, following a mathematical function to its inevitable conclusion.

Most software handles this with a Pseudo Random Number Generator, or PRNG. A PRNG is a formula that takes an internal value called a seed, applies a sequence of mathematical operations to it, and produces a number that looks random. It then updates its internal state and repeats. If you know the algorithm and the seed, you can predict every number it will ever produce. Not approximately. Perfectly. Every single one, in exact order, forever.

The seed is typically the system clock sampled at nanosecond precision when the generator initializes. Two computers seeded at exactly the same nanosecond, running the same algorithm, will produce identical sequences. The apparent randomness is entirely borrowed from the timing of initialization.

This is fine for most purposes — games, simulations, shuffling a Spotify playlist. But for anything that genuinely needs to be unpredictable, like generating encryption keys or running a national lottery, PRNGs are a liability. The solution is hardware. True Random Number Generators, or TRNGs, derive entropy from physical phenomena that are genuinely unpredictable.

FeaturePRNGTRNG
SourceMathematical formulaPhysical-world phenomena
Predictable?Yes, if you know the seedNo
SpeedExtremely fastSlower, requires hardware
Best useGames, simulations, general appsCryptography, security keys

Some TRNGs use atmospheric radio noise. Some measure the timing of radioactive particle decay. Some capture thermal noise from the microscopic chaotic movement of electrons in circuits. Cloudflare, which routes roughly 10% of all internet traffic, seeds its cryptographic keys by pointing a camera at a wall of lava lamps. The fluid dynamics of heating wax are chaotic enough to be mathematically uncompressible — you cannot predict the next frame from the previous one, which makes it a decent entropy source for something securing billions of HTTPS connections a day.

I was not hoping to beat a lava lamp. I was hoping to beat a browser widget.

What Google's Widget Is Actually Running

Since the widget runs inside a browser, it uses JavaScript's Math.random() and scales the output into the 1-10 range.

js
Math.floor(Math.random() * (max - min + 1)) + min
// for 1-10: Math.floor(Math.random() * 10) + 1

Math.random() returns a decimal between 0 (inclusive) and 1 (exclusive). Multiply by 10, take the floor, add 1, and you get an integer from 1 to 10. It's also worth noting that this approach is deliberately chosen over the simpler-looking % 10 because of something called modulo bias — if a generator's output range doesn't evenly divide by N, some outputs become statistically more likely than others. Scaling a float avoids that entirely.

The interesting part is what's running underneath Math.random(). In Chrome's V8 engine, which is what Google Search runs on, the implementation uses a member of the xorshift family of generators, specifically xorshift128+.

The name is a portmanteau of the operations it performs: XOR, rotate, shift. The "128" refers to the size of its internal state: 128 bits, split across two 64-bit words, s0 and s1. The "+" means it produces output by adding those two words together rather than XORing them, which improves the statistical quality of the lower-order bits and was one of the key refinements over earlier Xorshift variants.

On every call to Math.random(), the engine executes this sequence:

code
result  = s0 + s1          // output is computed first
s1     ^= s0               // XOR
s0      = rotl(s0, 24) ^ s1 ^ (s1 << 16)   // rotate + XOR
s1      = rotl(s1, 37)     // rotate
return result as float64 in [0, 1)

Every step is pure deterministic arithmetic. No randomness anywhere in the loop. The starting state is seeded once at page load — using the system clock at nanosecond precision, mixed with whatever entropy the OS has available — and everything that follows is an inevitable sequence of bit manipulations.

The 2015 Chrome Disaster

xorshift128+ wasn't always what Chrome used. And the reason it switched is where the story gets genuinely interesting.

Before 2015, V8's Math.random() ran on an algorithm called MWC1616 — a Multiply-With-Carry generator that combined two 16-bit state words into 32-bit outputs. By modern standards, it was embarrassingly weak.

Researchers ran MWC1616's output through BigCrush, a statistical test battery developed by Pierre L'Ecuyer and Richard Simard as part of the TestU01 library. BigCrush subjects a generator to roughly 160 distinct statistical tests — distribution uniformity tests, serial correlation tests, birthday spacing tests, linear complexity tests — designed to surface any deviation from true randomness. A strong generator is expected to pass all of them or very nearly all.

MWC1616 failed badly.

The cycle length — the number of outputs before the sequence repeats exactly — was around 40 million. That sounds large until you realize a busy browser tab can burn through tens of millions of math operations in minutes. Once the sequence cycled, applications would start receiving the exact same stream of numbers they'd already seen.

Worse, when researchers mapped consecutive MWC1616 outputs into 3D coordinate space (each output triplet becomes a point at coordinates x, y, z), the points didn't scatter uniformly through the space. They formed visible lines and walls — geometric structures called lattice patterns. This meant certain combinations of three consecutive numbers were mathematically impossible for the generator to produce. A generator that cannot generate certain outputs is, by definition, not uniform.

The V8 team replaced MWC1616 with xorshift128+. The difference in scale is difficult to overstate. The new algorithm's cycle length is 2¹²⁸ − 1:

code
340,282,366,920,938,463,463,374,607,431,768,211,455

At one billion calls per second, you would exhaust that period in roughly 10²² years. The universe is about 1.4 × 10¹⁰ years old. BigCrush found no statistical failures. The 3D lattice problem disappeared entirely.

Someone had already found the loophole. Browser engineers had patched it before I was old enough to have a DAA lab exam. I was hoping to find a version of that mistake that nobody had caught yet.

Testing It with Code

Theory felt insufficient. I tested it first in Python using Python's built-in random module, which runs on the MT19937 algorithm — the Mersenne Twister, a different and older PRNG with a 19937-bit state and a period of 2¹⁹⁹³⁷ − 1. Statistically robust, widely used, but not what Chrome runs.

Since Google's widget uses JavaScript, I rewrote the simulation in JS. You can run this directly in Chrome's developer console right now (F12 → Console):

js
const totalSimulations = 10_000_000;
const counts = Object.fromEntries(
  Array.from({ length: 10 }, (_, i) => [i + 1, 0])
);

for (let i = 0; i < totalSimulations; i++) {
  const result = Math.floor(Math.random() * 10) + 1;
  counts[result]++;
}

console.log("Number   | Occurrences    | Percentage");
console.log("----------------------------------------");
for (let num = 1; num <= 10; num++) {
  const pct = ((counts[num] / totalSimulations) * 100).toFixed(4);
  console.log(`${num}        | ${counts[num].toLocaleString().padEnd(14)} | ${pct}%`);
}

V8's JIT compiler runs 10 million iterations of this in under 50 milliseconds. The results:

NumberOccurrencesPercentage
11,000,79210.0079%
21,000,55610.0056%
3999,7379.9974%
41,001,40410.0140%
5999,1999.9920%
61,000,14210.0014%
7998,0359.9803%
8999,1249.9912%
9999,7529.9975%
101,001,25910.0126%

Every number hovers within a few hundredths of a percent of the theoretical 10.0%. The deviations are natural variance, not bias — the Law of Large Numbers says they flatten further as you scale up. At 10 billion iterations, those fractional differences approach zero. No number is favored. No hidden pattern surfaces. No exploit.

There's an irony buried here. Computers, which are constitutionally incapable of true randomness, produce a near-perfect uniform distribution. Humans, asked to "just pick a random number from 1 to 10," are terrible at it. Studies consistently show people pick 7 far more than any other number. They avoid 1 and 10 because they feel like boundaries, not choices. They avoid 5 because it's dead center and feels like a cop-out. Even numbers — 2, 4, 6, 8 — feel too structured. So everyone clusters around 3, 7, and 9, convinced they're being unpredictable.

A PRNG doesn't have feelings about the number 7. It's just arithmetic.

Wait — If It's Deterministic, Can't I Still Predict It?

This was the part that felt like a logical contradiction. If the algorithm is fully deterministic, with a known formula and a fixed state, why couldn't I just reverse-engineer the state and predict what was next?

Technically, under the right conditions, you can. Researchers have shown that given enough consecutive raw Math.random() outputs — specifically the full double-precision float values, not rounded integers — it's possible to reconstruct the internal state and predict every future output. The exact attack depends on implementation details, but the general idea is that each float64 output exposes part of the internal state through its mantissa bits:

code
// Math.random() outputs a float64 in [0, 1)
// The mantissa encodes observable bits of the generator's state
// With enough outputs, you can solve for s0 and s1
// and replay the sequence forward from any point
s0_bits ≈ output * 2^53   // extract state bits from mantissa

Once you have the state, every future output is known. Researchers have demonstrated this against xorshift-family generators. The important takeaway is that these generators are not cryptographically secure — they were never designed to be.

The problem is "right access conditions." The attack requires the raw float64 mantissa — values like 0.6473912847391... — not the integers the widget displays. When Google's generator shows me a 7, it has executed Math.floor(0.6473... * 10) + 1 and discarded 52 bits of precision that the reconstruction algorithm needs. Observing integers in the range 1-10 gives you roughly 3.32 bits of information per observation. The state is 128 bits. You'd need an unreasonable number of observations, and you'd also need to know the exact initialization time, the exact entropy mixed into the seed, and a clean way to extract raw floats from an interface that only shows you integers.

You don't get any of those. Not from a browser widget running inside Google Search.

I had three hours until my exam, ten programs left to study, and a rapidly expanding understanding of pseudo-random number generation that was becoming useless in real time.

Everything I'd learned was technically correct. PRNGs are deterministic. xorshift128+ can be analyzed. State reconstruction attacks exist and work under lab conditions. Predicting future outputs is possible if you have the right access.

I had accidentally skipped an important engineering lesson: theoretically possible is not the same as practically useful.

The Actual Punchline: C's rand() Would Have Been Easier

Here's what I didn't think to check.

The DAA lab programs I was worried about — Merge Sort, Quick Sort, Kruskal's, Dijkstra's — those are mostly written in C. C has its own random number function: rand(), seeded with srand().

The typical pattern looks like this:

c
srand(time(NULL));
int x = rand() % 10 + 1;

Most standard library implementations of rand() use a Linear Congruential Generator, or LCG. The formula is almost comically simple:

code
X(n+1) = (a × X(n) + c) mod m

Pick constants a, c, and m, and that's your entire random number generator. LCGs are fast, but they're famous for producing visible patterns and short cycles. They were already considered weak decades ago.

The seed for the code above is time(NULL), which returns the current time in whole seconds. Not nanoseconds. Seconds. There are 86,400 seconds in a day. If the lab software runs within a predictable window — say, during a two-hour exam — that's only around 7,200 possible seed values. A script could brute-force every single one in milliseconds, check which one matched the first few numbers generated, and predict every number that followed.

Compare that to Google's widget: seeded with nanosecond precision plus OS entropy, running xorshift128+ with a 2¹²⁸ − 1 period, designed specifically to resist state reconstruction.

I spent an hour trying to crack the hardest target in the room.

The vulnerable one was sitting right there in my DAA textbook.


I spent roughly an hour going down this rabbit hole. Another hour writing this. My exam is in 15 minutes and I haven't studied a single program. Probably fine — I now know more about random number generators than any of those ten experiments will teach me, which is almost certainly more useful than whatever 20-year-old sorting algorithm they've decided is worth examining.

Sources & Further Reading

  1. Yuri Chev's deep dive into Xorshift PRNGs https://yurichev.com/blog/xorshift/

  2. PwnFunction — How computers generate "random" numbers https://www.youtube.com/watch?v=dWwMYqT8u6g

  3. V8 Team — There's Math.random(), and then there's Math.random() https://v8.dev/blog/math-random

Contact Me

All fields are required. I typically respond within 1-2 business days, depending on the volume of messages. Looking forward to connecting with you!

If the portfolio, experiments, or open-source work have been useful, you can support the work here.