PHP Random Numbers

sa sahaya jerish Updated 17 Oct 2026
11 min read ·Lesson 56 of 109
In this post

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:

CategoryExamplesRequired Generator
Security-sensitiveOTPs, tokens, passwords, API keys, CSRF tokens, coupon codes with valueCryptographically secure (CSPRNG)
Not security-sensitiveShuffling a quiz, random banner, test data, simulations, games without stakesAny 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's random_int() and random_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 return 1, 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 $min is greater than $max, PHP throws a ValueError.
  • If the operating system cannot provide randomness (extremely rare), PHP throws a Random\RandomException (PHP 8.2+; an Exception in 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

  1. 10 ** 6 - 1 is 999999, the largest 6-digit number.
  2. random_int(0, 999999) gives every OTP from 000000 to 999999 an equal chance.
  3. str_pad(..., '0', STR_PAD_LEFT) keeps leading zeros, so 48213 becomes 048213. Without padding, OTPs starting with zero would be shorter, which confuses users and reduces the number of possible codes.
  4. 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 only 0–9 and a–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, and rtrim(..., '=') removes padding. 24 bytes encode to exactly 32 characters.

Typical Uses

UseTypical Size
Password reset / email verification links32 bytes
API keys32 bytes
CSRF tokens32 bytes
Remember-me tokens32 bytes (store a hash in the database)
File names for uploads16 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

MethodPHPPurpose
getInt($min, $max)8.2Random integer in a range
getBytes($length)8.2Random bytes
shuffleArray($array)8.2Returns a shuffled copy of an array
shuffleBytes($string)8.2Returns a string with its bytes shuffled
pickArrayKeys($array, $num)8.2Picks $num random keys
getBytesFromString($chars, $length)8.3Random string built from the given characters
nextFloat()8.3Random float in [0, 1)
getFloat($min, $max)8.3Random 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

  • Xoshiro256StarStar is 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 by mt_rand()) and Random\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.

FunctionAlgorithmSecure?Notes
rand($min, $max)Mersenne TwisterNoAlias of mt_rand() since PHP 7.1
mt_rand($min, $max)Mersenne TwisterNoSeedable with mt_srand()
mt_srand($seed)——Seeds the global Mersenne Twister
shuffle($array)Mersenne TwisterNoShuffles the array in place
array_rand($array, $n)Mersenne TwisterNoReturns random key(s)
str_shuffle($string)Mersenne TwisterNoShuffles characters
lcg_value()Combined LCGNoDeprecated in PHP 8.4
uniqid()Current time in microsecondsNo — not randomPredictable; 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, 80
1, 5, 91
2, 62
3, 73

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

  1. The weights add up to 100, so random_int(1, 100) draws a number from 1 to 100.
  2. 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.
  3. random_int() is used because rewards have monetary value; a predictable generator could be exploited.
  4. 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.
  5. The final throw can 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(), and new 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() and uniqid() for anything that needs to be unpredictable.

Official reference: https://www.php.net/manual/en/function.random-int.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about PHP Random Numbers and get answers drawn from it.

Signed-in readers only.