crypto.randomInt()

Returns a uniformly distributed random integer in [min, max) without modulo bias.

Signature
crypto.randomInt([min, ]max[, callback])
  • Added in Node.js v14.10.0, v12.19.0

Parameters

ParameterTypeDescription
$minoptionalinteger= 0Start of the random range, inclusive. Must be a safe integer.
$maxintegerEnd of the random range, exclusive. Must be a safe integer, and max - min must be less than 2**48.
$callbackoptionalFunctionOptional 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · javascript
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.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.