Random 8 digit number generator

Uniform eight-digit draws over all 100 million values, for order numbers and internal references, with leading zeros kept.

G

A hundred million values, and why that is smaller than it sounds

Eight digits give 100,000,000 values and 8 x log2(10) = 26.58 bits. As a keyspace that is negligible; as an identifier space it is comfortable for a small system and marginal for a busy one.

The number that matters is not the size of the space but the point at which two independent draws collide. That arrives at the square root of the space, not at the space itself. The chance of at least one duplicate reaches 1% at 1,418 references and 50% at 11,774. A shop that issues 12,000 random eight-digit order numbers is at a coin flip for having issued the same one twice.

The fix is not a bigger random draw on its own. It is a unique constraint in the database plus a retry on conflict, which costs almost nothing at these rates and turns a probabilistic problem into a deterministic one. Adding digits helps too, and the two together are what you actually want.

Chance of at least one duplicate among independent 8-digit references
References issuedProbability of a repeat
1,0000.5%
1,4181.0%
5,00011.7%
11,77450.0%

Random or sequential, and what each one leaks

Sequential order numbers are the default because they are free and they never collide. They also publish your volume. Anyone can place two orders a week apart, subtract the numbers, and read off how many orders you took in between. For a young company that is a number you may not want in a competitor spreadsheet.

Random references leak nothing about volume and cannot be walked by an attacker looking for orders that are not theirs, but they collide, and they carry no ordering, so you lose the ability to sort by reference as a proxy for time. If you need both properties, keep an internal sequential key for ordering and issue a separate random reference for anything customer-facing.

The trade-off, stated plainly
PropertySequentialRandom 8-digit
CollisionsNeverCoin flip at 11,774
Leaks volumeYes, by subtractionNo
Enumerable by an attackerYesNot usefully
Sorts by creation timeYesNo
Needs coordinationYes, a single counterNo

Eight digits is date-shaped, and something will notice

YYYYMMDD is eight digits, which means a random eight-digit reference occasionally looks exactly like a date. Between 1900 and 2099 there are 73,049 valid dates, so about 7.3 of every 10,000 references you issue are also a well-formed YYYYMMDD value. If any importer, log parser or reporting job in the pipeline is configured to try that format, those rows will silently change type.

The reverse mistake is more common and more damaging. Reference schemes built as a date plus a short counter are not random at all: the first eight characters are guessable from the order date, so the whole identifier reduces to the counter, and anyone with one reference can enumerate the neighbours. If a reference needs to be unguessable, none of it can be derived from the timestamp.

Prefix your references with a letter or a short code if they travel through systems that parse loose formats. An eight-character value that starts with a letter cannot be read as a date or coerced into an integer.

Fixed width and leading zeros

A reference number is a string of eight characters. 00481267 is a valid reference and one in ten begins with a zero. Store it as CHAR(8) rather than INT, and it stays eight characters through every export, every import and every JSON round trip.

A generator that draws an integer between 10000000 and 99999999 covers 90 million of the 100 million values and can never emit a leading zero. The entropy drops from 26.58 to 26.43 bits, but the practical damage is worse than the arithmetic: fixed-width references that are sometimes seven characters long break scanline formats, barcode encodings and every column alignment downstream.

Doing this in your own code

JavaScript
// 8 independent digits, uniform, leading zeros preserved.
// 250 is Math.floor(256 / 10) * 10. Bytes at or above it are thrown away,
// which is what keeps every digit at exactly 1 in 10.
function randomDigits(n) {
  const buf = new Uint8Array(1);
  let out = "";
  while (out.length < n) {
    crypto.getRandomValues(buf);
    if (buf[0] >= 250) continue;
    out += buf[0] % 10;
  }
  return out;
}

randomDigits(8);   // a 8-character string, never a number

The usual shortcut, Math.floor(Math.random() * 9e5) + 1e5, cannot produce a leading zero and draws from a PRNG whose state is recoverable from its output.

Python 3.6+
import secrets

# Draw the digits. This never becomes an int, so leading zeros survive.
"".join(secrets.choice("0123456789") for _ in range(8))

# Or draw one integer and pad it back to width.
f"{secrets.randbelow(10 ** 8):08d}"

random.randint() uses the Mersenne Twister and is not a security boundary. secrets draws from os.urandom.

Go
import (
    "crypto/rand"
    "fmt"
    "math/big"
)

limit := new(big.Int).Exp(big.NewInt(10), big.NewInt(8), nil)
v, err := rand.Int(rand.Reader, limit)
if err != nil {
    panic(err)
}

// The width verb is the part that keeps the leading zeros.
fmt.Printf("%08d\n", v)
Java
import java.security.SecureRandom;

SecureRandom rnd = new SecureRandom();
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
    sb.append(rnd.nextInt(10));
}
String value = sb.toString();

nextInt(bound) already rejects the unusable top of the range, so it is unbiased for a bound of 10.

PHP 7+
// random_int() is backed by the system CSPRNG.
// rand() and mt_rand() are not, and must not be used here.
$value = str_pad((string) random_int(0, 10 ** 8 - 1), 8, '0', STR_PAD_LEFT);
Shell
# 8 digits straight from the kernel CSPRNG.
# tr keeps only ASCII digits, so every byte that survives is uniform over 0-9.
LC_ALL=C tr -dc '0-9' < /dev/urandom | head -c 8; echo

Questions

How many 8-digit references can I issue before a collision?

The chance of at least one duplicate is 1% at 1,418 references and 50% at 11,774. Because collisions arrive near the square root of the space, 100 million values is not as much headroom as it looks. Add a unique constraint and retry on conflict.

Should order numbers be random or sequential?

Sequential never collides but publishes your order volume to anyone who places two orders and subtracts. Random leaks nothing but collides and does not sort. The usual answer is both: a sequential internal key and a separate random reference for anything the customer sees.

Can an 8-digit reference be mistaken for a date?

Yes. There are 73,049 valid YYYYMMDD dates between 1900 and 2099, so roughly 7 in every 10,000 random eight-digit values parse as one. Prefix references with a letter if they pass through systems that autodetect formats.

Does it produce numbers starting with zero?

Yes, at the correct rate of one in ten. Each digit is drawn independently over 0-9, so the full 00000000 to 99999999 range is covered. Store the result as a fixed-width string to keep it that way.

Is 26.58 bits enough to stop someone guessing a reference?

Not on its own. 26.58 bits is enumerable by any determined script. If knowing a reference grants access to something, put authentication in front of it rather than relying on the reference being hard to guess.

Related generators