Verifiable Randomness Documentation

Verifiable Randomness Systems

View the Project on GitHub blockrand-api/blockrand-js

Modulo Bias: Why Most Random Numbers in Games Are Slightly Wrong

This document explains a subtle but critical flaw in many game, casino, and simulation systems: modulo bias. It explains why it happens, how it breaks fairness, and how to eliminate it correctly.

The Problem Developers Think They’ve Solved

A very common pattern in game and backend code looks like this:

  1. Generate a random number
  2. Take modulo max
  3. Use the result
    • n := randomUint64() % max]
    • const n = cryptoRandom() % max;

The mental model is simple:

Unfortunately, this assumption is mathematically incorrect unless the range size is a power of two.

What Is Modulo Bias?

Modulo bias occurs when the total number of possible random values is not evenly divisible by the target range.

Example:

Assume a random source produces values from 0 to 9 (10 total values). You want a random number in the range 0 to 2 (3 values).

Mapping via modulo produces:

Final counts:

The distribution is biased. This bias never disappears, no matter how good the random source is.

Why This Matters in Games and Gambling

Modulo bias:

In casinos, even tiny statistical edges matter.

In games, this can affect:

If your system can be audited or replayed, even small bias is unacceptable.

Why “The Bias Is Small” Is the Wrong Argument

Developers often argue that the bias is negligible. This is false in systems that are:

If users can verify outcomes or replay them, any bias is a correctness bug, not an optimization tradeoff.

The Correct Solution: Rejection Sampling

The correct approach to mapping random numbers into ranges is rejection sampling.

Core idea:

  1. Generate a fixed-width random number (for example, 64 bits)
  2. Accept only values that fit evenly into the target range
  3. Reject and retry values that would introduce bias

How Rejection Sampling Works

Let:

Compute: The largest multiple of max that fits inside 2^64. Only accept random values below that threshold.

If a value falls outside it:

  1. Discard it
  2. Generate another

The accepted value modulo max is perfectly uniform.

Why This Works

Because every possible output value is backed by the exact same number of input values.

Performance Concerns (Addressed)

Rejection sampling sounds expensive but is not.

In practice:

This is safe even in high-throughput systems.

Why a Cryptographic RNG Alone Is Not Enough

Even if you use:

Modulo bias still occurs after the random number is generated. Uniform input does not imply uniform output. Correct mapping matters as much as entropy quality.

What a Fair Random Integer System Must Do

A correct implementation must:

  1. Never use modulo directly on raw random output
  2. Use rejection sampling
  3. Use fixed-width entropy
  4. Be deterministic if replay is required
  5. Be auditable after the fact

Key Takeaway

Fair randomness is not about better entropy. It is about correct mathematics. Most systems get this wrong. Correct systems are precise, boring, and provable. That is exactly what fairness should look like.

BlockRand always uses the correct mathematics to generate all its random numbers.