PHP Random Numbers
Random values are used throughout web applications: one-time passwords (OTPs), password reset tokens, API keys, session identifiers, coupon codes, shuffled quiz questions, A/B test assignment, sample
Random values are used throughout web applications: one-time passwords (OTPs), password reset tokens, API keys, session identifiers, coupon codes, shuffled quiz questions, A/B test assignment, sample data, and games.
These uses fall into two very different categories, and choosing the wrong tool for the first category is a serious security bug:
| Category | Examples | Required Generator |
|---|---|---|
| Security-sensitive | OTPs, tokens, passwords, API keys, CSRF tokens, coupon codes with value | Cryptographically secure (CSPRNG) |
| Not security-sensitive | Shuffling a quiz, random banner, test data, simulations, games without stakes | Any generator; a seedable one when results must be reproducible |
This lesson explains both, starting with the secure functions you should use by default.
What "Random" Means for a Computer
Computers are deterministic machines, so they produce random-looking numbers in one of two ways:
- Pseudo-random number generator (PRNG): an algorithm that produces a long sequence of numbers that look random, starting from an initial value called a seed. The same seed always produces the same sequence. PHP's
mt_rand()uses the Mersenne Twister PRNG. - Cryptographically secure PRNG (CSPRNG): a generator designed so that outputs cannot be predicted, even by someone who has seen many previous outputs. It draws entropy from the operating system (for example
getrandom()on Linux). PHP'srandom_int()andrandom_bytes()use the operating system's CSPRNG.
A normal PRNG is fine for shuffling a playlist. But if an attacker can observe some outputs of mt_rand(), they can reconstruct its internal state and predict future values — including "random" reset tokens. That is why security-sensitive values must come from a CSPRNG.
random_int() — Secure Random Integers
Syntax
random_int(int $min, int $max): int
Returns a cryptographically secure random integer between $min and $max, inclusive.
Example: Rolling a Die
<?php
for ($i = 1; $i <= 5; $i++) {
echo "Roll $i: " . random_int(1, 6) . "\n";
}
Output (example — values differ on every run)
Roll 1: 4
Roll 2: 1
Roll 3: 6
Roll 4: 4
Roll 5: 2
How It Works
- Both bounds are included, so
random_int(1, 6)can return1,6, or anything in between. - Every value in the range is equally likely.
random_int()avoids modulo bias internally (explained later), which naive approaches do not. - If
$minis greater than$max, PHP throws aValueError. - If the operating system cannot provide randomness (extremely rare), PHP throws a
Random\RandomException(PHP 8.2+; anExceptionin earlier versions).
Example: A 6-Digit OTP
<?php
function generateOtp(int $digits = 6): string
{
$max = 10 ** $digits - 1;
return str_pad((string) random_int(0, $max), $digits, '0', STR_PAD_LEFT);
}
echo generateOtp() . "\n";
echo generateOtp(4) . "\n";
Output (example)
048213
7302
Why It Is Written This Way
10 ** 6 - 1is999999, the largest 6-digit number.random_int(0, 999999)gives every OTP from000000to999999an equal chance.str_pad(..., '0', STR_PAD_LEFT)keeps leading zeros, so48213becomes048213. Without padding, OTPs starting with zero would be shorter, which confuses users and reduces the number of possible codes.- The OTP is returned as a string, because it is an identifier, not a quantity — leading zeros matter, and it should be compared with
hash_equals()or===.
Security note: An OTP is only safe with additional protections: store it hashed, expire it after a few minutes, and limit the number of attempts. A 6-digit code has only one million possibilities and can be brute-forced without attempt limits.
random_bytes() — Secure Random Bytes
Syntax
random_bytes(int $length): string
Returns a string of $length cryptographically secure random bytes. The bytes are binary data, not readable text, so they are usually encoded before storage or display.
Example: Tokens
<?php
$bytes = random_bytes(32);
$hexToken = bin2hex($bytes);
$urlToken = rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
echo $hexToken . "\n";
echo strlen($hexToken) . " characters\n";
echo $urlToken . "\n";
echo strlen($urlToken) . " characters\n";
Output (example)
9f1c2b7e4a0d3c8e5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a
64 characters
q3Vx-8kLm_Zp2Rt7YwNbXc4dEf9GhJs1
32 characters
How It Works
random_bytes(32)produces 32 bytes = 256 bits of randomness — far more than anyone can guess.bin2hex()turns each byte into two hexadecimal characters, so 32 bytes become 64 characters using only0–9anda–f.- For a shorter, URL-safe token, base64 encoding packs more data per character.
strtr(..., '+/', '-_')replaces the two characters that have special meaning in URLs, andrtrim(..., '=')removes padding. 24 bytes encode to exactly 32 characters.
Typical Uses
| Use | Typical Size |
|---|---|
| Password reset / email verification links | 32 bytes |
| API keys | 32 bytes |
| CSRF tokens | 32 bytes |
| Remember-me tokens | 32 bytes (store a hash in the database) |
| File names for uploads | 16 bytes |
For reset tokens and API keys, store a hash of the token (for example with hash('sha256', $token)) in the database rather than the token itself, so a database leak does not expose usable tokens.
The Random Extension: Random\Randomizer (PHP 8.2+)
PHP 8.2 introduced an object-oriented API for randomness. A Randomizer uses an engine (the algorithm) and provides convenient methods.
Creating a Randomizer
<?php
$randomizer = new Random\Randomizer();
Without arguments, it uses Random\Engine\Secure, which is the same CSPRNG as random_int(). Secure by default is the main advantage of this API.
Common Methods
| Method | PHP | Purpose |
|---|---|---|
getInt($min, $max) | 8.2 | Random integer in a range |
getBytes($length) | 8.2 | Random bytes |
shuffleArray($array) | 8.2 | Returns a shuffled copy of an array |
shuffleBytes($string) | 8.2 | Returns a string with its bytes shuffled |
pickArrayKeys($array, $num) | 8.2 | Picks $num random keys |
getBytesFromString($chars, $length) | 8.3 | Random string built from the given characters |
nextFloat() | 8.3 | Random float in [0, 1) |
getFloat($min, $max) | 8.3 | Random float in a range |
Example: Secure Random Password
<?php
$randomizer = new Random\Randomizer();
// Ambiguous characters (0, O, 1, l, I) are left out on purpose
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
$password = $randomizer->getBytesFromString($alphabet, 12);
$coupon = $randomizer->getBytesFromString('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', 8);
echo "Temporary password: $password\n";
echo "Coupon code: $coupon\n";
Output (example)
Temporary password: hT7kQm3xPzR9
Coupon code: K7MQ2XRH
getBytesFromString() (PHP 8.3+) picks each character uniformly at random from the alphabet using the secure engine. Removing look-alike characters makes codes easier to read and type.
For PHP 8.2 or earlier, the same result can be produced with random_int():
<?php
function randomString(string $alphabet, int $length): string
{
$result = '';
$lastIndex = strlen($alphabet) - 1;
for ($i = 0; $i < $length; $i++) {
$result .= $alphabet[random_int(0, $lastIndex)];
}
return $result;
}
Example: Shuffling and Picking
<?php
$randomizer = new Random\Randomizer();
$questions = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5'];
$shuffled = $randomizer->shuffleArray($questions);
$pickedKeys = $randomizer->pickArrayKeys($questions, 2);
echo implode(', ', $shuffled) . "\n";
echo implode(', ', array_map(fn($k) => $questions[$k], $pickedKeys)) . "\n";
echo implode(', ', $questions) . "\n";
Output (example)
Q3, Q5, Q1, Q4, Q2
Q2, Q5
Q1, Q2, Q3, Q4, Q5
shuffleArray()returns a new shuffled array; the original stays unchanged (last line).pickArrayKeys()returns random keys (in their original order), which you use to look up the values.
Reproducible Randomness: Seeded Engines
Sometimes you want the same "random" sequence every time — for tests, for generating demo data, or for procedurally generated content that must look the same for every visitor. Seeded engines provide this.
<?php
function sampleScores(int $seed): array
{
$randomizer = new Random\Randomizer(new Random\Engine\Xoshiro256StarStar($seed));
$scores = [];
for ($i = 0; $i < 5; $i++) {
$scores[] = $randomizer->getInt(40, 100);
}
return $scores;
}
var_dump(sampleScores(2026) === sampleScores(2026));
var_dump(sampleScores(2026) === sampleScores(2027));
Output
bool(true)
bool(false)
How It Works
Xoshiro256StarStaris a fast, high-quality seedable PRNG engine. The same seed (2026) produces the same sequence of numbers every time the script runs.- A different seed produces a different sequence.
- Other available engines include
Random\Engine\Mt19937(the Mersenne Twister, also used bymt_rand()) andRandom\Engine\PcgOneseq128XslRr64.
Warning: Seeded engines are predictable by design. Never use them for tokens, passwords, OTPs, or anything an attacker benefits from guessing.
Legacy Functions: rand(), mt_rand(), and Friends
You will encounter these functions in existing code.
| Function | Algorithm | Secure? | Notes |
|---|---|---|---|
rand($min, $max) | Mersenne Twister | No | Alias of mt_rand() since PHP 7.1 |
mt_rand($min, $max) | Mersenne Twister | No | Seedable with mt_srand() |
mt_srand($seed) | — | — | Seeds the global Mersenne Twister |
shuffle($array) | Mersenne Twister | No | Shuffles the array in place |
array_rand($array, $n) | Mersenne Twister | No | Returns random key(s) |
str_shuffle($string) | Mersenne Twister | No | Shuffles characters |
lcg_value() | Combined LCG | No | Deprecated in PHP 8.4 |
uniqid() | Current time in microseconds | No — not random | Predictable; only useful as a non-secret, roughly unique ID |
Example
<?php
mt_srand(100);
$first = [mt_rand(1, 10), mt_rand(1, 10), mt_rand(1, 10)];
mt_srand(100);
$second = [mt_rand(1, 10), mt_rand(1, 10), mt_rand(1, 10)];
var_dump($first === $second);
$colors = ['red', 'green', 'blue'];
shuffle($colors);
echo implode(', ', $colors) . "\n";
Output (the shuffled order varies)
bool(true)
green, red, blue
Seeding with the same value reproduces the same sequence, which demonstrates why these functions must not be used for secrets. They remain acceptable for non-security uses such as shuffling display order, but the Random\Randomizer API is the modern choice for new code — secure by default, and seedable only when you ask for it.
Random Floats
<?php
$randomizer = new Random\Randomizer();
$probability = $randomizer->nextFloat(); // [0, 1) — PHP 8.3+
$temperature = $randomizer->getFloat(20.0, 35.0); // PHP 8.3+
printf("%.4f\n", $probability);
printf("%.1f°C\n", $temperature);
Output (example)
0.7316
27.4°C
On older versions, mt_rand() / mt_getrandmax() is a common (non-secure) way to get a float between 0 and 1; random_int(0, PHP_INT_MAX) / PHP_INT_MAX is a secure alternative for simple cases.
Modulo Bias: Why Not % n?
A tempting way to get a number in a range is to take a large random number modulo the range size. This introduces bias when the generator's range is not an exact multiple of n.
Imagine a generator that returns 0–9 uniformly, and you want a number 0–3:
| Generated | % 4 |
|---|---|
| 0, 4, 8 | 0 |
| 1, 5, 9 | 1 |
| 2, 6 | 2 |
| 3, 7 | 3 |
Results 0 and 1 each appear for three inputs, but 2 and 3 for only two, so 0 and 1 are 50% more likely. With real generators the effect is smaller but still measurable — and unacceptable for cryptographic uses. random_int() and Randomizer::getInt() avoid this by rejecting values that would cause bias and drawing again. Always use their built-in range parameters instead of %.
Practical Example: Weighted Random Rewards
A promotion gives a random reward after each purchase: a 10% coupon most of the time, a 25% coupon less often, and free shipping rarely.
<?php
function weightedPick(array $weights): string
{
$total = array_sum($weights);
$roll = random_int(1, $total);
foreach ($weights as $item => $weight) {
$roll -= $weight;
if ($roll <= 0) {
return $item;
}
}
throw new LogicException('Weights must be positive integers.');
}
$rewards = [
'10% coupon' => 70,
'25% coupon' => 25,
'Free shipping' => 5,
];
$counts = array_fill_keys(array_keys($rewards), 0);
for ($i = 0; $i < 10000; $i++) {
$counts[weightedPick($rewards)]++;
}
foreach ($counts as $reward => $count) {
printf("%-14s %5d (%.1f%%)\n", $reward, $count, $count / 100);
}
Output (example — close to 70% / 25% / 5%)
10% coupon 7012 (70.1%)
25% coupon 2489 (24.9%)
Free shipping 499 (5.0%)
Step-by-Step Explanation
- The weights add up to
100, sorandom_int(1, 100)draws a number from 1 to 100. - The loop subtracts each weight in turn. A roll of 1–70 lands on the first reward, 71–95 on the second, and 96–100 on the third. Each reward's chance equals its weight divided by the total.
random_int()is used because rewards have monetary value; a predictable generator could be exploited.- The simulation runs 10,000 picks and counts the results, confirming that the actual distribution matches the configured weights. Dividing a count by 100 gives its percentage of 10,000.
- The final
throwcan only be reached if the weights are invalid (for example, zero or negative), which makes configuration mistakes visible instead of silently returning a wrong reward.
Common Mistakes
Using rand(), mt_rand(), or uniqid() for Tokens and Codes
Effect: Values can be predicted, allowing account takeover through reset links or OTPs.
Fix: Use random_int(), random_bytes(), or Random\Randomizer with the default secure engine.
Using md5(time()) or md5(uniqid()) as a "Random" Token
Effect: The input is guessable (based on time), so hashing it does not make it secret.
Fix: bin2hex(random_bytes(32)).
Losing Leading Zeros in Numeric Codes
Effect: OTPs like 048213 become 48213.
Fix: Generate codes as strings with str_pad().
Using % n to Fit a Range
Effect: Biased results.
Fix: Use the $min, $max parameters of random_int() or getInt().
Seeding the Generator for Security Values
Effect: Anyone who knows or guesses the seed can reproduce every value.
Fix: Never seed when generating secrets; secure generators do not need (or accept) seeds.
Using str_shuffle() to Generate Passwords
Effect: Not secure, and each character can appear at most as often as it does in the input string, which reduces the number of possible passwords.
Fix: Use getBytesFromString() or a random_int() loop.
Best Practices
- Default to
random_int(),random_bytes(), andnew Random\Randomizer()for all new code. - Use at least 16–32 random bytes for tokens and encode them with
bin2hex()or URL-safe base64. - Store hashes of tokens, not the tokens themselves, and expire them.
- Combine OTPs with expiry and attempt limits.
- Use seeded engines (
Xoshiro256StarStar) only for reproducible, non-security purposes such as tests and demo data. - Avoid deprecated and predictable functions such as
lcg_value()anduniqid()for anything that needs to be unpredictable.
Official reference: https://www.php.net/manual/en/function.random-int.php