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 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
// 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_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 $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
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 // 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
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 |
|---|---|---|
| No | Alias of |
| No | Games, shuffling, non-sensitive randomness |
| Yes | Verification codes, OTPs, lottery-style draws |
| Yes | API keys, tokens, session IDs (raw bytes) |
rand()andmt_rand()are identical since PHP 7.1 and are auto-seeded — manual seeding is unnecessary.Neither
rand()normt_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 anException.random_bytes($length)returns raw secure random bytes — encode them withbin2hex()orbase64_encode()before use.Always store and compare generated codes as padded strings, not raw integers, to avoid losing leading zeros.