crypto.randomBytes()

Generates cryptographically strong pseudorandom bytes as a Buffer, synchronously or via callback.

Signature
crypto.randomBytes(size[, callback])
  • Added in Node.js v0.5.8

Parameters

ParameterTypeDescription
$sizenumberThe number of bytes to generate. Must not be larger than 2**31 - 1.
$callbackoptionalFunctionOptional callback of the form (err, buf). When supplied, the bytes are generated asynchronously off the main thread.

Return value

BufferA Buffer of size random bytes when no callback is given; otherwise undefined, with the Buffer delivered to the callback.

Errors

Throws if size exceeds 2**31 - 1, or if there is not enough entropy available to satisfy the request.

Changelog

  • PHP v18.0.0Passing an invalid callback now throws ERR_INVALID_ARG_TYPE instead of ERR_INVALID_CALLBACK.
  • PHP v9.0.0Passing null as the callback argument now throws instead of being ignored.

crypto.randomBytes() is the primitive behind every secret your application generates. It draws from the operating system's CSPRNG, so the output is unpredictable even to an attacker who knows every other value you have produced — which is exactly what Math.random() cannot promise.

Sizing the secret

16 bytes (128 bits) is the floor for anything an attacker can attempt offline; 32 bytes (256 bits) is the comfortable default for session tokens, API keys and password-reset links. Encode with 'base64url' rather than 'hex' when the value lands in a URL: it is roughly a third shorter and needs no escaping.

Sync or async

The synchronous form blocks the event loop while the kernel fills the buffer. For typical sizes that is microseconds and entirely fine — generate a 32-byte token synchronously without guilt. The callback form moves the work to the threadpool and is worth using for unusually large requests, or on a boot path that is generating many keys at once.

What trips people up

The classic bug is randomBytes(n).toString('hex').slice(0, n), written in the belief that n bytes gives n characters. Hex doubles the length and base64 grows it by about a third, so slicing back down silently discards half your entropy. Decide how many bits you need first, then pick the byte count, and let the encoded string be whatever length it turns out to be.

Example

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

When to use it

  • Generating a 32-byte session token or API key for a new user
  • Producing a unique random salt before calling crypto.scryptSync()
  • Creating the initialization vector for an AES-GCM encryption
  • Minting a single-use password-reset token that expires after an hour

Frequently asked questions

How many bytes should a secure token be in Node.js?

32 bytes (256 bits) is the safe default for session tokens and API keys, and 16 bytes (128 bits) is the practical minimum for anything an attacker can guess at offline. Encode with base64url rather than hex to keep the string short without losing entropy.

Should I use the sync or async form of crypto.randomBytes()?

The synchronous form is fine for normal token sizes — it takes microseconds and blocking the event loop that briefly is not measurable. Use the callback form for very large requests or when generating many keys at once, since that work moves to the threadpool.

Why not just use Math.random() for tokens?

Math.random() is not cryptographically secure: its internal state can be recovered from a handful of outputs, letting an attacker predict every future value. crypto.randomBytes() draws from the operating system CSPRNG, which offers no such shortcut.

More examples

Password reset tokens: store the hash, send the secret

Hashing before storage means a database dump cannot be turned into working reset links, and deleting on use makes the token single-shot. The replay and forged lookups both return null, which is what an attacker gets.

Example · javascript
const crypto = require('node:crypto');

const resetTokens = new Map(); // stand-in for a database table

function issueResetToken(userId) {
  // 32 bytes = 256 bits. base64url is URL-safe with no escaping.
  const token = crypto.randomBytes(32).toString('base64url');

  // Store only a hash. A leaked database then yields no usable tokens.
  const lookup = crypto.hash('sha256', token);
  resetTokens.set(lookup, { userId, expiresAt: Date.now() + 3600_000 });

  return token; // emailed to the user, never logged
}

function consumeResetToken(token) {
  const record = resetTokens.get(crypto.hash('sha256', token));
  if (!record || record.expiresAt < Date.now()) return null;
  resetTokens.delete(crypto.hash('sha256', token)); // single use
  return record.userId;
}

const token = issueResetToken('user-42');
console.log('token chars   :', token.length);
console.log('first use     :', consumeResetToken(token));
console.log('replayed      :', consumeResetToken(token));
console.log('forged token  :', consumeResetToken('not-a-real-token'));

The truncation mistake

Slicing a hex string in half quietly cuts the search space from 2^128 to 2^64 — still large, but within reach of a determined attacker, and nothing in the code hints at the loss. base64url gives the same 128 bits in 22 characters instead of 32.

Example · javascript
const crypto = require('node:crypto');

// WRONG: looks like a 16-character token with 16 bytes of entropy.
const wrong = crypto.randomBytes(16).toString('hex').slice(0, 16);

// It actually has only 8 bytes = 64 bits, because hex doubles the length.
console.log('wrong length  :', wrong.length, 'chars');
console.log('wrong entropy :', (wrong.length / 2) * 8, 'bits');

// RIGHT: choose the byte count for the entropy you want.
const right = crypto.randomBytes(16).toString('hex');
console.log('right length  :', right.length, 'chars');
console.log('right entropy :', 16 * 8, 'bits');

// Shorter and still 128 bits, because base64url packs 6 bits per char.
const compact = crypto.randomBytes(16).toString('base64url');
console.log('base64url len :', compact.length, 'chars for', 16 * 8, 'bits');

Salt and IV generation

Both a salt and an IV must be unique per use but neither needs to be secret, so both are stored or transmitted in the clear next to the data. The lengths are the only stable output; the values differ on every run by design.

Example · javascript
const crypto = require('node:crypto');

// A fresh salt per password. Never share one salt across users.
const salt = crypto.randomBytes(16);
const derived = crypto.scryptSync('correct horse battery staple', salt, 32);

// Store salt alongside the derived key — it is not a secret.
const stored = `${salt.toString('hex')}:${derived.toString('hex')}`;
console.log('stored field parts:', stored.split(':').map((p) => p.length).join(' / '));

// A fresh IV per message. Reusing an IV with the same GCM key is fatal.
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update('secret message', 'utf8'), cipher.final()]);

console.log('salt bytes:', salt.length, '| iv bytes:', iv.length, '| key bytes:', key.length);
console.log('ciphertext bytes:', ct.length, '| tag bytes:', cipher.getAuthTag().length);

Discussion

  • Be the first to comment on this lesson.