PHPRandom Numbers

Random Numbers

PHP has had a way to generate random numbers since its earliest versions, but the "obvious" function for the job — rand() — is also the one you should reach for least often today. Over the years PHP has added better alternatives for different jobs: mt_rand() for faster general-purpose randomness, and random_int() and random_bytes() for anything that needs to be unpredictable in a security sense. Knowing which one to use, and why the difference matters, is the actual skill here — the function calls themselves are trivial.

rand() and mt_rand()

rand($min, $max) returns a pseudo-random integer between $min and $max inclusive. mt_rand($min, $max) does the same thing but is built on a different algorithm (a Mersenne Twister) that is both faster and statistically better distributed. As of PHP 7.1, rand() is actually just an alias that calls mt_rand() internally, so in modern PHP the two behave identically — mt_rand() still exists mainly for code that wants to be explicit about which generator it expects.

rand() and mt_rand()

PHP
<?php

echo rand(1, 6) . PHP_EOL;      // a dice roll, e.g. 4
echo mt_rand(1, 6) . PHP_EOL;   // also a dice roll, e.g. 2

// Calling with no arguments returns a number across the full int range
echo rand() . PHP_EOL;          // e.g. 1804289383
4
2
1804289383

Both functions are "pseudo-random": they use a mathematical formula seeded from an initial value to produce a sequence that only looks random. That is fine for dice games, shuffling a quiz, or picking a random background color — nothing depends on an attacker being unable to guess the next number in the sequence. It is not fine the moment unpredictability itself is the security property you need.

random_int() — cryptographically secure integers

PHP 7 introduced random_int($min, $max), which has the exact same signature as rand() and mt_rand() but draws from the operating system's cryptographically secure random source instead of a predictable formula. It is slightly slower than mt_rand() — that cost is the price of the underlying entropy source doing real work — but for almost every use case the difference is unmeasurable in practice. Since PHP 7 is now the baseline for any actively maintained codebase, there is rarely a good reason to reach for mt_rand() over random_int() even for non-security use cases.

random_int()

PHP
<?php

// Same signature as rand()/mt_rand(), but backed by a secure entropy source
$roll = random_int(1, 6);
echo $roll . PHP_EOL;

// random_int() can throw if the system can't supply enough entropy
try {
    $token = random_int(100000, 999999);
    echo $token . PHP_EOL;
} catch (Exception $e) {
    echo 'Could not generate a secure number: ' . $e->getMessage() . PHP_EOL;
}
5
482913
random_int() can throw
Unlike `rand()` and `mt_rand()`, `random_int()` is declared to throw an `Exception` if the underlying operating system random source is unavailable. In practice this almost never happens on a normal server, but wrapping calls in a `try`/`catch` is the technically correct thing to do — especially in code that must not silently fall back to a weaker source of randomness.
random_bytes() — raw secure randomness

When you need random data rather than a random number in a range — for example, raw bytes to build an API key, a session identifier, or a CSRF token — random_bytes($length) returns exactly that many bytes of cryptographically secure random binary data. It is the lower-level building block that random_int() itself relies on internally. Because the output is raw binary, you will usually encode it with something like bin2hex() or base64_encode() before storing or displaying it.

random_bytes() for tokens

PHP
<?php

$rawBytes = random_bytes(16);
echo bin2hex($rawBytes) . PHP_EOL;   // 32 hex characters = 16 bytes

// A common pattern: a URL-safe API key
$apiKey = bin2hex(random_bytes(32));
echo $apiKey . PHP_EOL;
9f1c3a7d2e5b0f48a6c1d9e7b3f28a05
7b2e9f4c1a8d3e6f0b5c2a9d7e1f4b8c6a0d3e7f1b5c9a2d4e8f0b6c3a1d9e7f
Never use rand() or mt_rand() for security-sensitive values
`rand()` and `mt_rand()` are not cryptographically secure — their output can, in some cases, be predicted by an attacker who observes enough previous values or knows roughly when the process started, because the underlying algorithm is deterministic once seeded. Never use them to generate password reset tokens, email verification codes, session identifiers, API keys, or anything else where guessability is a security risk. Use `random_int()` for numeric values in a range, and `random_bytes()` for raw tokens, every time.
Seeding behavior

Older PHP code sometimes calls srand() or mt_srand() to explicitly seed the generator before calling rand()/mt_rand(), often with something like time(). This used to matter because early PHP versions could produce repeatable sequences if never seeded. Since PHP 7.1, rand() and mt_rand() are auto-seeded internally the first time they are called, so manually seeding them is unnecessary in modern code — and doing it with a predictable seed like the current timestamp actually makes the sequence easier to guess, not harder. random_int() and random_bytes() don't expose any seeding function at all, by design: their whole point is that you can't influence or predict their output.

Seeding is unnecessary (and risky) in modern PHP

PHP
<?php

// Old, unnecessary pattern — don't do this
mt_srand(time());
echo mt_rand(1, 100) . PHP_EOL;

// Modern PHP: just call it, no seeding needed
echo mt_rand(1, 100) . PHP_EOL;
Worked example: a secure verification code

Put this together into something realistic: generating a six-digit numeric verification code to email or text to a user, similar to what a login-confirmation or password-reset flow would send. Because this code grants access if guessed, it must come from random_int() rather than rand().

Generating a six-digit verification code

PHP
<?php

function generateVerificationCode(): string
{
    // random_int is inclusive on both ends, so this covers
    // every six-digit code from 000000 through 999999
    $code = random_int(0, 999999);

    // Pad with leading zeros so the result is always 6 digits long
    return str_pad((string) $code, 6, '0', STR_PAD_LEFT);
}

$code = generateVerificationCode();
echo "Your verification code is: {$code}" . PHP_EOL;
Your verification code is: 042817

The str_pad() call matters here: random_int(0, 999999) will sometimes return a number like 4213, and without padding that would show up as a four-digit code instead of a consistent six-digit one. Storing and comparing the padded string version (not the raw integer) avoids that inconsistency entirely, and it also means the code is naturally compared as a string against whatever the user types back in.

Function

Secure?

Typical use

rand($min, $max)

No

Alias of mt_rand() since PHP 7.1

mt_rand($min, $max)

No

Games, shuffling, non-sensitive randomness

random_int($min, $max)

Yes

Verification codes, OTPs, lottery-style draws

random_bytes($length)

Yes

API keys, tokens, session IDs (raw bytes)

  • rand() and mt_rand() are identical since PHP 7.1 and are auto-seeded — manual seeding is unnecessary.

  • Neither rand() nor mt_rand() is safe for anything security-related; their output is not truly unpredictable.

  • random_int($min, $max) is the secure drop-in replacement for a random number in a range, and can throw an Exception.

  • random_bytes($length) returns raw secure random bytes — encode them with bin2hex() or base64_encode() before use.

  • Always store and compare generated codes as padded strings, not raw integers, to avoid losing leading zeros.

Tip
As a rule of thumb: if the randomness only needs to look unpredictable to a human — a game, a shuffle, a random color scheme — `mt_rand()` is fine and slightly faster. The moment the randomness needs to be unpredictable to a determined attacker — anything tied to authentication, account recovery, or access control — use `random_int()` or `random_bytes()` without exception. Mixing the two up is one of the most common small security mistakes in PHP codebases, and it is entirely avoidable.