crypto.randomBytes()
Generates cryptographically strong pseudorandom bytes as a Buffer, synchronously or via callback.
crypto.randomBytes(size[, callback])- Added in Node.js v0.5.8
Parameters
| Parameter | Type | Description |
|---|---|---|
$size | number | The number of bytes to generate. Must not be larger than 2**31 - 1. |
$callbackoptional | Function | Optional 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
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.
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.
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.
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