How Do Computers Generate Random Numbers — Are They Really Random?
Computers do make random numbers, but not all of them are random in the same way. Some sequences are generated by deterministic math, while others start from physical noise that the machine cannot predict. If you need a quick generator for testing, demos, or just to poke at a range, try our free random number generator.
Random usually means one of two things
In practice, “random” on a computer means either pseudo-random or cryptographically secure. A pseudo-random number generator, or PRNG, uses an algorithm plus a starting value called a seed. Give it the same seed and you get the same output every time, which is a feature when you want repeatable tests.
A cryptographic generator is different. It draws on entropy from the real world, like timing jitter or device noise, then mixes that into a stream of bytes that is hard to predict. That matters for passwords, session tokens, API keys, and anything an attacker could guess if the pattern leaks.
The important bit: “random enough” is context-dependent. If you are shuffling cards in a game or sampling rows in a test fixture, a PRNG is usually fine. If you are generating secrets, it is the wrong tool unless it is backed by a secure source from the operating system.
How a pseudo-random generator actually works
A PRNG is a machine that takes some input state, runs a math function, and spits out the next number in a sequence. A simple example is a linear congruential generator, which uses a formula like state = (a * state + c) % m. That output can look noisy, but under the hood it is completely deterministic.
Because it is deterministic, PRNGs are easy to reproduce. That is useful when debugging a simulation or a game replay, because the same seed gives the same run. It also means they are not safe for security if the algorithm or seed can be guessed.
In JavaScript, the built-in Math.random() is convenient but not suitable for secrets. It is fine for UI effects, approximate sampling, or casual testing. For anything security-related, use the platform crypto API instead, because the browser or runtime is supposed to provide stronger entropy.
Here is the basic shape of the difference:
// Good for repeatable tests, not for secrets
function lcg(seed) {
let state = seed;
return () => {
state = (1664525 * state + 1013904223) % 4294967296;
return state / 4294967296;
};
}
const rand = lcg(12345);
console.log(rand()); // same result every time
console.log(rand());
Where true randomness comes from
Real-world randomness is messier than code. A computer can sample tiny timing variations, thermal noise, disk behavior, mouse movement, or clock jitter, then turn that noise into entropy. Operating systems collect these signals and feed them into a randomness pool or entropy source.
Most software never talks to hardware noise directly. Instead, it asks the OS for random bytes, and the OS decides how to mix and stretch the available entropy. On Linux that often means reading from /dev/urandom for practical use, while modern browsers expose secure randomness through APIs like crypto.getRandomValues().
That distinction matters because raw physical noise is not instantly useful. It needs to be sampled, whitened, mixed, and checked so the output is not biased by the source. The result is not magic, just engineering that turns a flaky physical process into something usable.
True randomness is usually not “every number is equally mystical.” It is “the system collected enough unpredictable noise that the next value is not realistically guessable.”
When random numbers need to be reproducible
There is a whole class of work where repeatability beats unpredictability. Simulations, Monte Carlo runs, unit tests, procedural generation, and fuzzing often need a stable seed so you can reproduce a failure. If a test breaks once in a thousand runs, you want to replay the exact sequence, not chase a ghost.
That is why many tools let you set a seed manually. You can log the seed with your test output, then rerun the exact same random path later. The numbers are still “random-looking,” just not random in the strict mathematical sense.
A common workflow looks like this:
- Pick a seed value, such as
8675309. - Generate a sequence for your test data or simulation.
- Save the seed alongside the result.
- Reuse the seed when you need to debug the same case again.
This is also why developers sometimes get confused when two machines produce different results. If one environment seeds from the current time and another uses a fixed value, the output diverges immediately. The generator is behaving correctly; the inputs are different.
For more on how secure randomness shows up in browser workflows, see our guide to cryptographically safe random strings.
Why security uses a different kind of random
Security is where weak randomness gets expensive. If an attacker can predict the next token, they can hijack a session, brute-force a reset link, or infer a key. That is why passwords, CSRF tokens, session IDs, API keys, and UUID-like secrets should come from a secure generator, not a toy PRNG.
A secure generator tries to make prediction useless even if someone sees earlier outputs. The internal state should be hard to infer, and the output should be mixed enough that patterns do not leak. In browser code, that usually means using crypto.getRandomValues() rather than rolling your own math.
Here is a simple browser example:
// Browser-only, secure enough for tokens and IDs
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
console.log(bytes);
// Example: Uint8Array(16) [ 204, 12, 91, 44, 233, 18, 250, 77, ... ]
If you need a numeric range, the usual pattern is to generate bytes, map them into the range, and reject out-of-range values to avoid bias. That last part matters. A sloppy % operation can skew results if the byte range does not divide evenly into your target range.
Why random-looking output can still be biased
Not every stream that looks noisy is actually good randomness. Some generators fail simple tests: they repeat too quickly, show patterns in low bits, or cluster values more than they should. A visual check can fool you because human brains are excellent at finding patterns that are not there, and equally bad at noticing subtle bias.
For example, if you generate numbers from 1 to 10,000 by taking random bytes and doing byte % 10000, the distribution is wrong unless the source space lines up cleanly. The fix is usually rejection sampling: discard values outside a usable range and draw again. It is a little less elegant and a lot less broken.
Another common issue is seeding from time alone. If two processes start at the same millisecond, they can produce the same “random” sequence. That is great for reproducing bugs and terrible for uniqueness.
When you are choosing a generator, ask three questions:
- Do I need to reproduce the same sequence later?
- Could an attacker benefit from predicting the next value?
- Does the output need to be uniformly distributed across a range?
If the answer to the second question is yes, use secure randomness. If the answer to the first is yes, use a seeded PRNG. If you only need to spin up test values fast, a simple generator is usually enough.
See It in Action
Suppose you are testing an endpoint that accepts a 6-digit verification code. You need codes that look random, but you also want to reproduce failures when a test breaks. That is a classic split between pseudo-random generation and secure generation.
Before:
// Bad for debugging, bad for security if reused carelessly
function code() {
return Math.floor(Math.random() * 1000000)
.toString()
.padStart(6, '0');
}
console.log(code());
That gives you something like:
047281
903115
009604
After, for reproducible tests:
function seededCode(seed) {
let state = seed;
return () => {
state = (1103515245 * state + 12345) % 2147483648;
return String(state % 1000000).padStart(6, '0');
};
}
const nextCode = seededCode(42);
console.log(nextCode());
console.log(nextCode());
console.log(nextCode());
And for production secrets, do not reuse that logic. Use a secure random source and map it carefully into your format. A verification code that an attacker can predict is just a login wall with a hole in it.
Frequently Asked Questions
Are computer random numbers really random?
Sometimes, but often not in the strict sense. Many are pseudo-random, which means they are generated by deterministic algorithms and only look random. For security work, the useful question is whether they are unpredictable to an attacker.
What is the difference between PRNG and true random numbers?
A PRNG starts from a seed and produces a repeatable sequence using math. True random numbers come from physical entropy, like timing jitter or electronic noise. PRNGs are good for simulations and tests; true random sources are better for secrets.
Is Math.random() secure?
No. It is designed for convenience, not for cryptography or token generation. If you need secure values in the browser, use crypto.getRandomValues() instead.
Why do developers seed random number generators?
Seeding makes a sequence repeatable, which is helpful for debugging and tests. If a bug only appears with one random path, a saved seed lets you recreate it exactly. That is one of the few times “not random” is the point.
The Bottom Line
Computers generate random numbers in two broad ways: deterministic algorithms that imitate randomness, and secure sources that mix in unpredictable physical noise. The first is great for tests, simulations, and repeatable workflows. The second is the one you want for secrets, tokens, and anything that should not be guessed.
If you are just checking a range, making test data, or verifying a UI flow, keep it simple. If you are building anything sensitive, use the operating system or browser crypto APIs and avoid hand-rolled shortcuts. And if you want to generate a few values without wiring up code, give the random number generator a spin.
Use the right kind of randomness for the job. The wrong one usually works right up until it does not.