Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild

Generating a Random String in Node.js Without Getting the Security Wrong

Sean

Platform Writer

Sep 23, 2026
7 min read

Generating a random string in Node.js has a right answer that depends entirely on what the string is for. For a display id, a throwaway key in a demo, or anything nobody could exploit by predicting it, Math.random and a character-picking loop work and cost nothing to write. For anything used as a session token, an API key, a password reset link, or a verification code, Math.random is the wrong tool regardless of how the loop looks, because its output is predictable enough to attack, and Node’s built-in crypto module, specifically randomBytes, randomUUID, and randomInt, is the actual answer.

Generating a Random String in Node.js Without Getting the Security Wrong

This covers Node’s crypto-based options for random strings, why Math.random fails specifically for tokens rather than generally, and which of crypto’s several functions fits which use case, with runnable examples for each.

Table of contents

Why Math.random is the wrong tool for anything security-sensitive

Math.random in JavaScript is a pseudo-random number generator built for speed and statistical distribution, not for secrecy. V8’s implementation produces a sequence that is, in principle, predictable: knowing a handful of consecutive outputs is enough, in published research, to reconstruct the generator’s internal state and predict every value it produces afterward. That is irrelevant for shuffling an array or picking a random UI color, and it is exactly the property an attacker needs if the ‘random’ string is a password reset token, a session id, or an API key: predict the next output and you have predicted the next valid token.

The practical failure mode is not usually someone reverse-engineering V8 directly; it is a token space that turns out to be smaller than it looks. A character-picking loop over Math.random with a wide alphabet at a reasonable length sounds like a lot of combinations, and the character-level randomness is fine for guessing resistance against brute force, but the underlying generator is not designed to resist an attacker who can observe or influence its state, which cryptographic randomness is specifically built to withstand.

crypto.randomBytes: the general-purpose secure random string

crypto.randomBytes reads from the operating system’s cryptographically secure random source, /dev/urandom on Linux and macOS, the Windows CryptoGenRandom API on Windows, and returns a Buffer of however many bytes you ask for. Converting that buffer to a string is the last step, and the encoding you pick changes the string’s length and character set, covered in the encoding section below.

const crypto = require('crypto');

function generateToken(bytes = 32) {
  return crypto.randomBytes(bytes).toString('hex');
}

console.log(generateToken()); // 64 hex characters, 256 bits of entropy

The byte count you pass in is the actual security parameter; the string length is a side effect of the encoding. 32 bytes is 256 bits of entropy, comfortably more than needed for any token an application issues, and a reasonable default for API keys and session tokens where the cost of a longer string is negligible.

crypto.randomUUID: the built-in when a UUID is enough

crypto.randomUUID(), available in Node without any import beyond crypto itself since Node 14.17, generates a version 4 UUID using the same secure random source as randomBytes underneath. It is the right call whenever the requirement is simply a unique, hard-to-guess identifier in the standard UUID shape: a request id for tracing, a resource id in a URL, a row’s primary key value when the application generates ids instead of the database.

const crypto = require('crypto');

const id = crypto.randomUUID();
console.log(id); // e.g. '3b241101-e2bb-4255-8caf-4136c566a962'

A UUID carries about 122 bits of randomness, plenty for a secret token too, but the fixed format, 36 characters with four dashes, is less convenient than a plain hex or base64 string when the caller needs to control the exact length, the more common reason to reach for randomBytes instead for tokens specifically.

crypto.randomInt: secure random numbers, not just strings

For a numeric code, a one-time password sent by SMS or email, crypto.randomInt(min, max) is the function to reach for instead of building one out of randomBytes by hand. It returns a uniformly distributed integer in the given range using the same secure source, and it specifically avoids the modulo bias that a naive Math.random-based range calculation introduces when the range does not divide the generator’s output space evenly.

const crypto = require('crypto');

function generateOtp() {
  return crypto.randomInt(100000, 999999).toString();
}

console.log(generateOtp()); // a random 6-digit code, uniformly distributed

Generating the code securely is only half the job; storing and checking it matters just as much. A six-digit OTP has only a million possible values, small enough that an attacker who can submit guesses freely will eventually land on the right one, so the check needs a rate limit on attempts per code, not just cryptographic randomness on generation. Store the code hashed rather than in plain text, the same discipline as a password, and expire it after a short window so a leaked value stops being useful quickly.

Choosing an encoding: hex, base64, and base64url

The same 32 bytes from randomBytes produce a different-length string depending on encoding, and the choice matters when the string is going into a URL, a cookie, or a database column with a fixed width.

  • hex doubles the byte length, 32 bytes becomes 64 characters, using only 0-9 and a-f. Safe everywhere, including places that only accept lowercase alphanumerics, but the least compact option.
  • base64 encodes roughly 4 characters for every 3 bytes, so 32 bytes becomes about 44 characters, but includes plus, slash, and equals padding characters that need escaping in a URL.
  • base64url, Node’s URL-safe variant, replaces the unsafe characters and drops the padding, giving the same compactness as base64 with a string that is safe to drop directly into a URL path or query parameter unescaped.

For a password reset link or an email verification token, where the string ends up in a URL a user clicks, base64url is almost always the right encoding: shorter than hex for the same entropy, and none of the escaping problems plain base64 has in a URL.

Picking the right function for the job

Put together, the choice comes down to what the string is actually for, not a single best function:

  • Session tokens, API keys, password reset links: crypto.randomBytes with base64url encoding, 32 bytes for the byte count.
  • Resource ids, request ids, database primary keys generated in code: crypto.randomUUID, for the standard shape and the tooling that already expects it.
  • Numeric OTP codes: crypto.randomInt, specifically to avoid the modulo bias a hand-rolled loop introduces.
  • Anything non-sensitive, a display placeholder, a random UI seed, test fixture data: Math.random is fine, precisely because nothing depends on it being unguessable.

Wherever the generated string ends up, an API key stored in an environment variable, a token written into a Postgres column for later lookup, the generation itself is the easy part; storing it safely afterward, out of source control and set as a real environment variable on whatever service reads it, matters just as much as which crypto function produced it. Our guide to building a Node backend covers where a generated secret like this actually lives once the service is running, and the same discipline applies to Python’s own secrets module for teams generating tokens on both sides. Keeping a generated secret out of a committed Dockerfile is covered directly in Dockerfile secrets handling, worth reading before a token generated correctly ends up baked into an image layer anyway.

How this fits the rest of the stack

A cryptographically random token is only as safe as where it lives afterward: hardcoded in a Dockerfile, committed to a repository, or printed to logs undoes the whole point of crypto.randomBytes in the first place. Environment variables on a deployed service are the normal place for it, set once in the dashboard and never in source, and the RunxBuild hosting calculator prices the Node service that would actually run this code next to it.

Useful related references:

FAQ

Why is Math.random bad for generating tokens in Node.js?

Math.random uses a fast, non-cryptographic generator whose output is, in principle, predictable from a handful of previous values. That is fine for shuffling an array, and wrong for anything where predicting the next value lets someone guess a valid session token, API key, or reset link.

What is the difference between crypto.randomBytes and crypto.randomUUID in Node?

randomBytes returns raw random bytes you encode yourself, hex or base64url usually, giving full control over the resulting string’s length and format. randomUUID returns a fixed-format version 4 UUID using the same secure source. Use randomBytes for tokens where you control the length; use randomUUID where a standard UUID shape is expected.

How do I generate a URL-safe random string in Node.js?

crypto.randomBytes(32).toString(‘base64url’) gives a compact, URL-safe string with no characters that need escaping in a query parameter or path, unlike plain base64 which includes plus signs, slashes, and padding characters that break unescaped in a URL.

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

32 bytes, giving 256 bits of entropy, is a reasonable default for session tokens and API keys and is far more than needed against any realistic brute-force attempt. Shorter tokens, 16 bytes for instance, are still reasonably strong but leave less margin as an application scales.

Is crypto.randomInt better than Math.random for numeric codes?

Yes, for two reasons: it draws from a cryptographically secure source instead of a predictable one, and it specifically avoids the modulo bias that a naive Math.random-based range calculation introduces when the range does not divide the generator’s output evenly.

#generate random string nodejs#node crypto randombytes#node crypto randomuuid#secure random token nodejs#math random vs crypto