crypto.randomInt()
Returns a uniformly distributed random integer in [min, max) without modulo bias.
crypto.randomInt([min, ]max[, callback])- Added in Node.js v14.10.0, v12.19.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$minoptional | integer= 0 | Start of the random range, inclusive. Must be a safe integer. |
$max | integer | End of the random range, exclusive. Must be a safe integer, and max - min must be less than 2**48. |
$callbackoptional | Function | Optional callback of the form (err, n). When supplied, the integer is generated asynchronously. |
Return value
numberA random integer n such that min <= n < max, when no callback is given; otherwise undefined, with the value delivered to the callback.
Errors
Throws ERR_OUT_OF_RANGE if max is not greater than min, if the range is 2**48 or larger, or if either bound is not a safe integer.
Changelog
- PHP v18.0.0Passing an invalid callback now throws ERR_INVALID_ARG_TYPE instead of ERR_INVALID_CALLBACK.
crypto.randomInt() returns an integer in [min, max) — min inclusive, max exclusive, the same convention as Array.prototype.slice(). Called with one argument it means [0, max), so randomInt(6) gives 0 to 5 and a six-sided die is randomInt(1, 7).
Why not do the arithmetic yourself
The obvious hand-rolled version, randomBytes(1)[0] % 6, is subtly wrong. A byte spans 0–255, which is not divisible by 6, so the values 0–3 come up slightly more often than 4 and 5. That is modulo bias, and it is measurable given enough samples. crypto.randomInt() handles it correctly by discarding and redrawing values that fall in the biased tail, giving a genuinely uniform distribution.
When to reach for it
Any time the choice must be unguessable: OTP digits, shuffle order in a game or draw, a random element of a lottery, jitter that an attacker should not be able to predict. For ordinary non-security randomness — animation jitter, sampling a log line, picking a placeholder image — Math.random() is faster and perfectly adequate.
What trips people up
Forgetting that max is exclusive is the near-universal off-by-one. randomInt(1, 6) never returns 6. The range is also capped: max - min must be under 248, and both bounds must be safe integers, so this cannot produce a random BigInt.
Example
When to use it
- Generating the digits of a six-digit one-time passcode
- Picking a random winner from an array of entrants in a prize draw
- Shuffling a deck with an unbiased Fisher-Yates swap
- Choosing unpredictable retry jitter that an attacker cannot anticipate
Frequently asked questions
Is max inclusive or exclusive in crypto.randomInt()?
Exclusive, matching slice() and array indexing — crypto.randomInt(1, 7) returns 1 through 6. Pass max + 1 when you want the upper bound included.
Why use crypto.randomInt() instead of Math.floor(Math.random() * n)?
Math.random() is not cryptographically secure, so its future output can be predicted from a few observed values, and naive scaling of a random byte also introduces modulo bias. crypto.randomInt() is both unpredictable and uniform, which matters for OTPs, draws and shuffles.
What is the largest range crypto.randomInt() supports?
max - min must be less than 2**48, and both bounds must be JavaScript safe integers. For larger ranges, generate raw bytes with crypto.randomBytes() and build a BigInt yourself, taking care to reject values in the biased tail.
More examples
One-time passcodes and random picks
Passing the array length straight to randomInt() gives a valid index every time, since the exclusive upper bound is exactly what an index needs. The padStart matters: without it one OTP in ten renders shorter than six digits and users report it as a bug.
const crypto = require('node:crypto');
// A 6-digit OTP, zero-padded so 000042 stays six characters.
function otp() {
return String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
}
// Unbiased pick from any array.
function pick(items) {
return items[crypto.randomInt(items.length)];
}
// A recovery code from an unambiguous alphabet (no 0/O, no 1/I/l).
const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
function recoveryCode(length = 10) {
let out = '';
for (let i = 0; i < length; i++) out += ALPHABET[crypto.randomInt(ALPHABET.length)];
return out;
}
console.log('otp digits :', otp().length);
console.log('pick is a member :', ['a', 'b', 'c'].includes(pick(['a', 'b', 'c'])));
console.log('recovery code length:', recoveryCode().length);
console.log('code uses safe alphabet:', [...recoveryCode()].every((c) => ALPHABET.includes(c)));Modulo bias, demonstrated
With 600,000 samples the biased version shows roughly 100,000 for faces 0-3 and about 97,600 for 4 and 5 — a spread near 2,400 that no amount of sampling averages away. The randomInt spread stays in the low hundreds, which is ordinary sampling noise.
const crypto = require('node:crypto');
const N = 600_000;
const count = (fn) => {
const t = new Array(6).fill(0);
for (let i = 0; i < N; i++) t[fn()]++;
return t;
};
// Biased: 256 is not divisible by 6, so 0-3 win the extra draws.
const biased = count(() => crypto.randomBytes(1)[0] % 6);
// Unbiased: randomInt rejects and redraws values in the skewed tail.
const fair = count(() => crypto.randomInt(6));
const spread = (t) => Math.max(...t) - Math.min(...t);
console.log('biased counts:', biased.join(' '));
console.log('fair counts :', fair.join(' '));
console.log('biased spread:', spread(biased));
console.log('fair spread :', spread(fair));An unbiased Fisher-Yates shuffle
The i + 1 is the crux — using randomInt(i) would exclude i and produce a provably non-uniform shuffle where no element can stay in place. The popular sort-with-random-comparator trick is worse still: its distribution depends on the engine's sort algorithm.
const crypto = require('node:crypto');
function secureShuffle(items) {
const a = [...items];
// Walk backwards, swapping each element with one at or before it.
for (let i = a.length - 1; i > 0; i--) {
const j = crypto.randomInt(i + 1); // inclusive of i, hence + 1
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
const deck = Array.from({ length: 52 }, (_, i) => i);
const shuffled = secureShuffle(deck);
console.log('same length :', shuffled.length === deck.length);
console.log('same cards :', [...shuffled].sort((x, y) => x - y).join() === deck.join());
console.log('order changed :', shuffled.join() !== deck.join());
// NOT a shuffle: sort with a random comparator is badly non-uniform.
console.log('avoid: arr.sort(() => Math.random() - 0.5)');
Discussion