Random Seeds and Reproducibility

May 28, 20266 min read

True randomness is great for security tokens but terrible for reproducibility. When you write a unit test that depends on random values, run a Monte Carlo simulation that colleagues need to verify, or generate procedural content in a game that players share via seed codes, you need deterministic pseudorandomness. A seeded PRNG produces the same sequence of values every time you initialize it with the same seed, making random behavior reproducible.

Why Reproducibility Matters

Three scenarios demand seeded randomness:

Testing: A randomized test that fails intermittently is a debugging nightmare. Fixed seeds let you reproduce the exact failure every time.

// Test with fixed seed — always reproducible
test('shuffle preserves all elements', () => {
  const rng = new SeededPRNG(42)
  const deck = [1, 2, 3, 4, 5]
  const shuffled = fisherYatesShuffle(deck, rng)
  expect(shuffled).toEqual([3, 1, 5, 2, 4])  // Deterministic
})

// Test with random seed logged on failure — reproducible after the fact
test('randomized partition invariant', () => {
  const seed =.Date.now()
  const rng = new SeededPRNG(seed)
  try {
    // ... test logic using rng ...
  } catch (e) {
    e.message += ` (seed: ${seed})`
    throw e
  }
})

Simulations: Scientific and financial simulations must be reproducible for peer review and regulatory compliance. If your Monte Carlo simulation produces different results every run, no one can verify your conclusions.

Game development: Games like Minecraft use world seeds so players can share identical procedurally generated worlds. The same seed always produces the same terrain, structures, and biome distribution.

Choosing a PRNG Algorithm

Not all PRNGs are equal. Here is how the common options compare:

AlgorithmPeriodSpeedStatistical QualityUse Case
LCG (linear congruential)2³²Very fastPoorAvoid
xorshift322³²−1Very fastModerateSimple games
xorshift128+2¹²⁸−1Very fastGoodGeneral purpose
PCG322⁶⁴FastExcellentRecommended
Mersenne Twister (MT19937)2¹⁹⁹³⁷−1ModerateExcellentScientific computing
xoshiro256**2²⁵⁶−1Very fastExcellentModern standard

For JavaScript applications, xoshiro256** provides the best balance of speed, quality, and reasonable state size. Here is a compact implementation:

class Xoshiro256SS {
  constructor(seed) {
    // SplitMix32 to expand seed into 4 state values
    this.state = [0, 0, 0, 0]
    for (let i = 0; i < 4; i++) {
      seed += 0x9e3779b9
      let t = seed ^ (seed >>> 16)
      t = Math.imul(t, 0x21f0aaad)
      t ^= t >>> 15
      t = Math.imul(t, 0x735a2d97)
      t ^= t >>> 15
      this.state[i] = t >>> 0
    }
  }

  next() {
    const [s0, s1, s2, s3] = this.state
    const result = Math.imul(s1, 5) >>> 0
    const rotated = ((result << 7) | (result >>> 25)) >>> 0

    const t = (s1 << 17) >>> 0
    this.state[2] = (this.state[2] ^ t) >>> 0
    this.state[0] = (s0 ^ s2) >>> 0
    this.state[1] = (s1 ^ s3) >>> 0
    this.state[2] = (s2 ^ s0) >>> 0
    this.state[3] = (s3 ^ s1) >>> 0

    this.state[0] = (this.state[0] ^ (this.state[0] >>> 16)) >>> 0

    return (rotated >>> 0) / 4294967296
  }
}

const rng = new Xoshiro256SS(12345)
rng.next()  // 0.4690172740712762 — same every time with seed 12345

Seed Generation and Management

The seed itself should be random in production but fixed in tests. A common pattern:

const DEFAULT_SEED = 42

function createRNG(overrides = {}) {
  const seed = overrides.seed ?? DEFAULT_SEED
  return new Xoshiro256SS(seed)
}

// Production: random seed
const productionRNG = createRNG({ seed: crypto.getRandomValues(new Uint32Array(1))[0] })

// Testing: fixed seed
const testRNG = createRNG({ seed: 42 })

For simulations that run across multiple machines, store the seed alongside results:

function runSimulation(config) {
  const seed = crypto.getRandomValues(new Uint32Array(1))[0]
  const rng = new Xoshiro256SS(seed)
  const results = computeModel(rng, config)

  return {
    seed,       // Required for reproduction
    config,     // Full configuration
    results,    // Output data
    timestamp: new Date().toISOString(),
  }
}

Common Pitfalls

  • Seeding with timestamps: Using Date.now() as a seed produces different values per millisecond, but two simulations started in the same millisecond share a seed. Use crypto.getRandomValues() for production seeds instead.
  • State mutation across tests: If you share a single PRNG instance across tests, the second test depends on how many values the first test consumed. Create a fresh PRNG per test.
  • Floating-point precision: Different languages handle float conversion differently. rng.next() / 2³² may produce slightly different values in JavaScript vs. Python due to double precision. For cross-language reproducibility, work with integer outputs only.
  • State serialization: Long-running simulations may need to checkpoint. Serialize the full PRNG state array so you can resume from the exact point of interruption.
// Checkpoint and restore
const checkpoint = [...rng.state]
// ... later ...
const restored = new Xoshiro256SS(0)
restored.state = checkpoint

Practice seeded random number generation with the tool at /tools/random-number, which supports both standard and seeded modes with selectable algorithms and exportable seed values.