Toolmingo
Guides6 min read

Random Number Generator: How It Works and When to Use It

Learn how random number generation works, the difference between true random and pseudo-random numbers, and how to generate random numbers in JavaScript, Python, Go, and more.

Generating a random number sounds trivial — computers do it billions of times per second. But the difference between a pseudo-random number from your programming language and a cryptographically secure random number can mean the difference between a working app and a serious security vulnerability.

This guide explains how random number generation works, when each type matters, and how to implement it correctly across popular languages.

True random vs. pseudo-random

All random numbers your computer generates fall into one of two categories:

Type Source Speed Use case
Pseudo-random (PRNG) Mathematical algorithm seeded with a value Very fast Simulations, games, shuffling, non-security use
Cryptographically secure (CSPRNG) Hardware entropy (keystrokes, network timing, /dev/urandom) Slightly slower Passwords, tokens, session IDs, cryptography

Key point: A PRNG starts from a seed value and produces a deterministic sequence. Same seed → same sequence every time. This is useful for reproducible simulations but completely wrong for security. A CSPRNG gathers genuine entropy from hardware and is unpredictable even if the attacker knows the algorithm.

Most bugs come from using Math.random() (or its equivalent) for things that need crypto.getRandomValues().

How pseudo-random number generators work

The most common PRNG algorithm is Mersenne Twister, used by Python's random module, Ruby, PHP, and many others. It works by:

  1. Taking a seed (often the current timestamp in milliseconds)
  2. Applying a series of bitwise operations to produce the next number
  3. Repeating to generate a sequence with a period of 2^19937 − 1

The output looks random but is mathematically predictable. Given enough output values, an attacker can reconstruct the internal state and predict all future values.

Linear congruential generators (LCG) are simpler and older:

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

Where a, c, and m are carefully chosen constants. java.util.Random uses this. These are fast but statistically weaker than Mersenne Twister.

Generating random numbers in code

JavaScript / TypeScript

// Basic: random float between 0 (inclusive) and 1 (exclusive)
Math.random()  // e.g. 0.7291...

// Integer in range [min, max] (inclusive)
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

randomInt(1, 6)   // Simulates a dice roll
randomInt(0, 99)  // Random percentage

// Cryptographically secure (browser & Node.js)
const array = new Uint32Array(1);
crypto.getRandomValues(array);
const secureInt = array[0] % 100;  // 0–99

// Node.js (cleaner API)
import { randomInt } from 'node:crypto';
randomInt(1, 101)  // 1–100 inclusive, cryptographically secure

Never use Math.random() for: passwords, tokens, API keys, session IDs, lottery draws, or anything security-sensitive.

Python

import random
import secrets  # For cryptographically secure generation

# Basic PRNG
random.random()          # Float: [0.0, 1.0)
random.randint(1, 10)    # Integer: 1–10 inclusive
random.uniform(0, 100)   # Float: [0.0, 100.0]
random.choice([1, 2, 3, 4, 5, 6])  # Random item from list

# Seed for reproducibility (useful in tests/simulations)
random.seed(42)
random.randint(1, 100)  # Always 52 with seed 42

# Cryptographically secure
secrets.randbelow(100)     # 0–99
secrets.choice([1, 2, 3])  # Secure random choice
secrets.token_hex(16)      # 32-char hex string (for tokens)

secrets was added in Python 3.6 specifically to replace random in security-sensitive contexts.

Go

import (
    "math/rand"
    "crypto/rand"
    "math/big"
    "time"
)

// Basic PRNG (Go 1.20+ auto-seeds; older versions need manual seed)
n := rand.Intn(100)          // 0–99
f := rand.Float64()          // [0.0, 1.0)
rand.Seed(time.Now().UnixNano())  // Manual seed for Go < 1.20

// Cryptographically secure
max := big.NewInt(100)
n, err := rand.Int(rand.Reader, max)  // crypto/rand.Reader
if err != nil {
    panic(err)
}
// n.Int64() is now a secure random integer 0–99

In Go, math/rand and crypto/rand are separate packages — the distinction is explicit in the import path.

PHP

// Basic PRNG
rand(1, 100);      // Integer 1–100
mt_rand(1, 100);   // Mersenne Twister (better distribution)

// Cryptographically secure (PHP 7+)
random_int(1, 100);   // Secure integer
random_bytes(16);     // 16 secure random bytes (for tokens)

// Generate a secure token
bin2hex(random_bytes(16));  // 32-char hex string

rand() in PHP was famously weak for many years. Always use random_int() in PHP 7+ for anything security-related.

Common use cases and best practices

Picking a random item from a list

// JavaScript
const items = ['apple', 'banana', 'cherry'];
const pick = items[Math.floor(Math.random() * items.length)];

// Python
import random
pick = random.choice(items)

// Go
pick := items[rand.Intn(len(items))]

Shuffling an array

The correct algorithm is Fisher-Yates shuffle (also called Knuth shuffle). Naive approaches (like sorting by random keys) produce biased results:

// Correct: Fisher-Yates
function shuffle(array) {
  const arr = [...array];  // don't mutate original
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

// Wrong: biased distribution!
array.sort(() => Math.random() - 0.5);

The sort-based approach is biased because the comparison function must be consistent, but random values aren't. V8's Array.sort is stable but the random comparisons still cause uneven distribution of outcomes.

Generating a random password or token

// Node.js — secure token generation
import { randomBytes } from 'node:crypto';
const token = randomBytes(32).toString('hex');  // 64-char hex

// Or URL-safe base64
const urlToken = randomBytes(32).toString('base64url');  // ~43 chars

// Secure random password
import { randomInt } from 'node:crypto';
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%';
const password = Array.from({ length: 16 }, () =>
  charset[randomInt(0, charset.length)]
).join('');

Weighted random selection

Sometimes you need "random but not equally likely" — like a game where common items drop 70% of the time and rare items 5%:

function weightedRandom(items) {
  // items: [{ value: 'common', weight: 70 }, { value: 'rare', weight: 5 }, ...]
  const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
  let random = Math.random() * totalWeight;

  for (const item of items) {
    random -= item.weight;
    if (random <= 0) return item.value;
  }
  return items[items.length - 1].value;
}

const loot = weightedRandom([
  { value: 'coin', weight: 70 },
  { value: 'sword', weight: 25 },
  { value: 'dragon-egg', weight: 5 },
]);

Seeding for reproducible results

In data science, machine learning, and testing, you often want random but reproducible results:

import random
import numpy as np

# Python
random.seed(42)
np.random.seed(42)

# Running with the same seed produces the same sequence every time
# Useful for: reproducible train/test splits, debugging, shared benchmarks

Set the seed at the start of your script/test. Document which seed you used so collaborators can reproduce your results.

Range and distribution

Math.random() produces a uniform distribution — all values equally likely. Real-world data often follows different distributions:

Distribution Use case Library
Uniform Default, fair dice, simple ranges Built-in
Normal (Gaussian) Heights, test scores, errors numpy.random.normal()
Exponential Time between events, network packets numpy.random.exponential()
Poisson Event counts per interval numpy.random.poisson()
Binomial Coin flips, A/B test outcomes numpy.random.binomial()

For simulations, pick the distribution that matches the real phenomenon you're modelling.

Generate random numbers instantly

Instead of writing boilerplate code, use Toolmingo's Random Number Generator to instantly generate one or multiple random numbers with a custom range, and List Randomizer to shuffle any list in one click.

FAQ

Q: Is Math.random() truly random? No. It's pseudo-random — generated by a deterministic algorithm. The output is statistically uniform (passes randomness tests) but not unpredictable. For most non-security uses (games, UI, simulations) this is perfectly fine.

Q: Can two calls to Math.random() return the same value? Theoretically yes, but the probability is astronomically low. For a 64-bit float, you'd expect a collision only after generating billions of values. In practice, don't rely on uniqueness — use UUIDs when you need unique identifiers.

Q: What's the best way to generate a random number between 1 and 10? Math.floor(Math.random() * 10) + 1 in JavaScript, random.randint(1, 10) in Python. The +1 pattern is important — Math.random() never returns exactly 1.0, so without it you'd get 0–9, not 1–10.

Q: Why is array.sort(() => Math.random() - 0.5) wrong? Because Array.sort expects a consistent comparator: if a > b in one call, it must always be > b. A random comparator violates this contract, causing non-uniform shuffle distributions. Use Fisher-Yates instead.

Q: How random is "random"? What's the period of common PRNGs? Mersenne Twister (Python, PHP, Ruby): period 2^19937 − 1 — effectively infinite for any practical use. Java java.util.Random: period 2^48. JavaScript V8's Math.random() (xorshift128+): period 2^128. You will never exhaust these sequences in a real application.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools