Random strings power everything from temporary file names to invitation codes, request IDs, and security tokens. This guide explores practical techniques in both the browser and Node.js, explains where each approach shines, and shows how to avoid subtle pitfalls like bias and insufficient entropy.
Quick starts (copy-paste)
// Browser (secure): URL-safe random string via Web Crypto
function randomStringUrlSafe(length = 32) {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
// Map 256 values into 64 URL-safe chars without bias using rejection sampling
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; // base64url set (64 chars)
const mask = 0b00111111; // 63
let out = "";
let carry = 0;
let bits = 0;
for (let i = 0; i < bytes.length; i++) {
carry = (carry << 8) | bytes[i];
bits += 8;
while (bits >= 6) {
out += alphabet[(carry >> (bits - 6)) & mask];
bits -= 6;
}
}
if (bits) out += alphabet[(carry << (6 - bits)) & mask];
return out.slice(0, length);
}
console.log(randomStringUrlSafe(32));
URL-safe, high-entropy, no padding, and easy to copy into URLs, cookies, filenames, etc.
// Node.js (secure): hex token
import { randomBytes } from "node:crypto";
function randomHex(lengthBytes = 16) { // 16 bytes = 32 hex chars (\~128 bits)
return randomBytes(lengthBytes).toString("hex");
}
console.log(randomHex()); // e.g. "f3b9d4f0a6b2c8..."
Hex is compact, fast, ASCII-only, and universally supported.
// Convenience (non-secure): quick ID with Math.random
// Good for demos, test fixtures, or UI keys — NOT for secrets or collisions-sensitive IDs.
function quickId() {
return Math.random().toString(36).slice(2); // base-36, variable length
}
console.log(quickId());
Fast and tiny, but not cryptographically secure and less robust against collisions.
Why random strings (not numbers)?
- Transportable: Strings are easy to pass in URLs, HTTP headers, cookies, filenames, and logs.
- Encoding choices: Hex, base64, and URL-safe alphabets fit different constraints.
- Human-friendliness: You can design alphabets that avoid ambiguous glyphs (O/0, l/1, etc.).
Approach 1: Math.random() (non-security)
Math.random()
is fine for non-security use
cases like ephemeral UI keys, dummy data, and demos. It
is not suitable for tokens, passwords, or
anything that must resist guessing.
Base-36 shortcut
function randomBase36(length = 16) {
let s = "";
while (s.length < length) s += Math.random().toString(36).slice(2);
return s.slice(0, length);
}
Custom alphabet (simple)
function randomStringSimple(length = 16, alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") {
let out = "";
for (let i = 0; i < length; i++) {
const idx = Math.floor(Math.random() * alphabet.length);
out += alphabet[idx];
}
return out;
}
Note: The
Math.floor(Math.random()*N)
approach is
acceptable for casual uses, but don’t use it for
security.
Approach 2: Web Crypto API (secure)
In modern browsers and Deno, use
crypto.getRandomValues
for
cryptographically secure randomness.
Secure hex string (browser)
function randomHexBrowser(bytes = 16) {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
let out = "";
for (let i = 0; i < arr.length; i++) {
out += arr[i].toString(16).padStart(2, "0");
}
return out;
}
Secure string from a custom alphabet (bias-free)
Mapping random bytes into an alphabet with rejection sampling avoids modulo bias.
function secureRandomString(length = 21, alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") {
if (!window.crypto || !crypto.getRandomValues) {
throw new Error("Secure Web Crypto not available in this environment.");
}
const output = [];
const bytes = new Uint8Array(length * 2); // oversample; we'll likely reject some
const maskLimit = Math.floor(256 / alphabet.length) * alphabet.length; // largest multiple of |alphabet| <= 256
while (output.length < length) {
crypto.getRandomValues(bytes);
for (let i = 0; i < bytes.length && output.length < length; i++) {
const b = bytes[i];
if (b < maskLimit) {
output.push(alphabet[b % alphabet.length]);
}
}
}
return output.join("");
}
Approach 3: UUIDs (standards-based IDs)
UUIDv4 gives 122 bits of randomness in a recognizable
format like
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
.
Browser
const id = (crypto.randomUUID) ? crypto.randomUUID() : (function fallback() {
// Fallback: still use secure randomness where possible
const bytes = new Uint8Array(16);
(crypto.getRandomValues ? crypto.getRandomValues(bytes) : bytes.fill(0)); // last resort zeros
// Set version (4) and variant (10)
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes].map(b => b.toString(16).padStart(2, "0")).join("");
return hex.slice(0,8)+"-"+hex.slice(8,12)+"-"+hex.slice(12,16)+"-"+hex.slice(16,20)+"-"+hex.slice(20);
})();
console.log(id);
Node.js
import { randomUUID } from "node:crypto";
console.log(randomUUID());
Approach 4: Node.js crypto (secure)
Node’s crypto
module is the go-to for
servers and scripts.
Hex / base64 tokens
import { randomBytes } from "node:crypto";
const hex32 = randomBytes(16).toString("hex"); // 16 bytes -> 32 hex chars (\~128 bits)
const b64 = randomBytes(24).toString("base64"); // 24 bytes -> 32 base64 chars (+padding)
console.log({ hex32, b64 });
URL-safe base64 (a.k.a. base64url) without padding
import { randomBytes } from "node:crypto";
function randomBase64Url(bytes = 32) {
return randomBytes(bytes)
.toString("base64")
.replace(/+/g, "-")
.replace(///g, "\_")
.replace(/=+\$/g, "");
}
console.log(randomBase64Url());
Bias-free custom alphabet (Node)
import { randomBytes } from "node:crypto";
function randomStringNode(length = 21, alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") {
const out = [];
const size = Math.ceil(length * 1.1); // oversample a bit
const maskLimit = Math.floor(256 / alphabet.length) \* alphabet.length;
while (out.length < length) {
const buf = randomBytes(size);
for (let i = 0; i < buf.length && out.length < length; i++) {
const b = buf[i];
if (b < maskLimit) out.push(alphabet[b % alphabet.length]);
}
}
return out.join("");
}
console.log(randomStringNode());
Encodings: hex, base64, base64url, and URL-safe strings
- Hex: 2 characters per byte (compact, ASCII, no symbols). Great for logs and filenames.
-
Base64: ~1.33 chars per byte
(shorter than hex). Not URL-safe due to
+
,/
, and=
. -
Base64url: Replace
+
→-
,/
→_
, strip=
. Ideal for URLs, cookies, JWT-like identifiers. -
Custom alphabets: Design for
readability (omit
0OIl
) or specific constraints.
Browser: base64 and base64url from secure bytes
function randomBase64(bytes = 16) {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
// Convert to base64 using binary string + btoa
let binary = "";
arr.forEach(b => binary += String.fromCharCode(b));
return btoa(binary);
}
function randomBase64Url(bytes = 16) {
return randomBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
How long should my string be? (Entropy math)
Security strength comes from entropy (bits of
uncertainty), not just length. A string of length
L
over an alphabet of size
N
has approximately
L × log2(N)
bits of entropy
(assuming unbiased randomness).
function bitsOfEntropy(length, alphabetSize) {
return length * Math.log2(alphabetSize);
}
// Examples:
console.log(bitsOfEntropy(32, 16)); // 32 hex chars ≈ 128 bits
console.log(bitsOfEntropy(22, 64)); // 22 base64 chars ≈ 132 bits
Target at least ~128 bits for long-lived tokens; ~96–112 bits can suffice for short-lived, rate-limited identifiers.
Choosing lengths quickly
-
Hex: 32 chars ≈ 128 bits
(
randomBytes(16).toString("hex")
). - Base64url: 22 chars ≈ 132 bits (from 16 bytes then trimmed/padded as needed).
- Base62 (A-Z a-z 0-9): 22 chars ≈ 131 bits.
Deterministic (seeded) random strings for tests
For reproducible tests and fixtures, use a small seeded PRNG. Never use this for secrets.
// Mulberry32: tiny deterministic PRNG (not crypto-secure)
function mulberry32(seed) {
let t = seed >>> 0;
return function() {
t += 0x6D2B79F5;
let r = Math.imul(t ^ (t >>> 15), 1 | t);
r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
function seededString(length = 16, seed = 1234, alphabet = "abcdefghijklmnopqrstuvwxyz") {
const rnd = mulberry32(seed);
let out = "";
for (let i = 0; i < length; i++) {
out += alphabet[Math.floor(rnd() * alphabet.length)];
}
return out;
}
console.log(seededString(12, 42)); // same output for the same seed
Common pitfalls & best practices
-
Don’t use
Math.random()
for secrets. Use Web Crypto or Nodecrypto
for anything security-sensitive. - Avoid modulo bias. If mapping bytes to an alphabet, use rejection sampling like the examples above.
- Prefer URL-safe output for IDs that may travel through links, cookies, or filenames—use base64url or custom safe alphabets.
- Track entropy targets. Choose lengths that reach ~128 bits for long-lived tokens.
-
Time and locale independence.
Don’t mix timestamps or predictable fields into
“random” IDs unless you clearly separate the
random portion (for tracing you can prefix:
<time>_<rand>
). -
Performance tips. Generate once
per need; batch where appropriate. In browsers,
getRandomValues
is fast and vectorized. - Collision handling. Even with high-entropy IDs, treat collisions as possible: handle “duplicate key” errors gracefully and retry.
-
Ambiguous characters. For human
entry, consider alphabets that remove
0OIl1
.
Cheat sheet
Goal | Browser | Node.js |
---|---|---|
Secure hex (128-bit) |
randomHexBrowser(16)
|
randomBytes(16).toString("hex")
|
Secure base64url |
randomBase64Url(16)
|
randomBase64Url(16)
|
UUID v4 |
crypto.randomUUID()
|
randomUUID()
|
Human-friendly |
secureRandomString(…,"ABCDEFGHJKLMNPQRSTUVWXYZ23456789")
|
randomStringNode(…,"ABCDEFGHJKLMNPQRSTUVWXYZ23456789")
|
Non-secure demo |
randomBase36()
|
Math.random()…
|
Deterministic for tests |
seededString()
|
seededString()
|