Random numeric string generator
Digits only, uniform over every value including those that start with zero. Each digit carries 3.321928 bits.
More options
A numeric string is not a number
This is the distinction the whole page rests on. A ten-digit numeric string is drawn uniformly from all 10,000,000,000 strings of that length, which includes 0000000000 and every value that begins with a zero. Exactly 10% of them do. A ten-digit random number, by the usual reading, is drawn from 1,000,000,000 to 9,999,999,999 and can never begin with a zero.
The consequences show up at the boundary where the string stops being a string. Number("0473918205") returns 473918205, which is nine digits, and the comparison against the stored code then fails. A CSV opened in Excel or Google Sheets strips the leading zero on import unless the column is typed as text or the value is prefixed with an apostrophe. A JSON field typed as a number does the same. An integer database column normalises 004182 to 4182 and the verification silently breaks for one code in ten.
Keep it a string end to end. Use a char or varchar column, quote it in JSON, format the CSV column as text, and compare with a string comparison.
If verification codes fail for roughly one user in ten and nobody can reproduce it, a leading zero being parsed away is the first thing to check.
Precision, if you do parse it anyway
JavaScript numbers are IEEE 754 doubles, so integers are exact only up to Number.MAX_SAFE_INTEGER, which is 9,007,199,254,740,991. That is sixteen digits. A numeric string of sixteen digits or more parsed into a JavaScript number can round to a different value, and because most JSON parsers produce doubles, this happens in transit rather than in your own code.
Ten digits is comfortably inside that limit, so the only problem at this length is the leading zero. At sixteen digits and beyond both problems apply at once, and the answer is the same as before: leave it as a string, or use BigInt if arithmetic is genuinely required.
The entropy is low, and saying so is the point
log2(10) is 3.321928, so a ten-digit string is 33.22 bits. NIST SP 800-63B-4 treats anything below 64 bits as requiring rate limiting, and anything below 32 bits is broken outright: 2 to the 31 MD5 guesses takes about ten milliseconds on one current GPU.
That places a six-digit OTP at 19.93 bits, which is comfortably in the broken band. This is not a criticism of OTPs. A six-digit code is safe precisely because it is rate-limited and short-lived, and the security comes from the limit rather than from the code. It is worth stating plainly, because a tool that labels a six-digit code "strong" is teaching the wrong lesson.
If you need a digit-only value that resists offline guessing, the length has to be substantial. Twenty digits reaches 66.44 bits and twenty-five reaches 83.05. Thirty-nine digits are needed for 128 bits, at which point a different alphabet is the better answer.
| Digits | Bits | Keyspace | Band |
|---|---|---|---|
| 4 | 13.29 | 10,000 | Broken. A PIN, safe only behind a lockout |
| 6 | 19.93 | 1,000,000 | Broken. A standard OTP, safe only behind rate limiting |
| 8 | 26.58 | 100,000,000 | Broken |
| 10 | 33.22 | 10,000,000,000 | Weak |
| 20 | 66.44 | 10^20 | Moderate |
| 39 | 129.56 | 10^39 | Cryptographic |
Where digits-only is a requirement rather than a preference
Anything entered on a telephone keypad or read aloud over a phone line, where letters are ambiguous or impossible. Numeric-only database columns and fixed-width legacy record formats, which are still common in banking and telecoms interchange. Fields validated against a digits-only pattern. Test fixtures standing in for account numbers, meter readings or reference numbers where the shape has to look right.
One thing this generator deliberately does not do is compute check digits. A value that has to pass a Luhn check, an ISO 7064 check or an IBAN validation is not a uniform random string, because the final digit is a function of the others. Generate the body here and compute the check digit yourself, and remember that the check digit contributes no entropy.
Doing this in your own code
JavaScript
// A numeric STRING, so leading zeros survive.
function randomDigits(length) {
const digits = "0123456789";
const limit = 4294967296 - (4294967296 % 10);
const buf = new Uint32Array(1);
let out = "";
for (let i = 0; i < length; i++) {
let x;
do { crypto.getRandomValues(buf); x = buf[0]; } while (x >= limit);
out += digits[x % 10];
}
return out;
}
randomDigits(10); // e.g. "0473918205" - the leading zero is real Number(randomDigits(10)) throws that leading zero away and is the single most common bug with numeric codes.
Python 3.6+
import secrets
# As a string, zeros preserved
"".join(secrets.choice("0123456789") for _ in range(10))
# A six digit OTP, zero padded
f"{secrets.randbelow(10**6):06d}" # "004182" is a valid code Java 8+
import java.security.SecureRandom;
SecureRandom rng = new SecureRandom();
// Six digit OTP, zero padded
String otp = String.format("%06d", rng.nextInt(1_000_000));
// Ten digit string
StringBuilder sb = new StringBuilder(10);
for (int i = 0; i < 10; i++) sb.append((char) ('0' + rng.nextInt(10))); PHP 7+
<?php
// Zero padded, kept as a string throughout
$otp = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$code = '';
for ($i = 0; $i < 10; $i++) {
$code .= (string) random_int(0, 9);
} SQL
-- Store numeric codes as text, never as an integer type.
CREATE TABLE verification_codes (
id bigserial PRIMARY KEY,
code char(6) NOT NULL, -- not integer: 004182 must stay 004182
expires_at timestamptz NOT NULL
);
-- An integer column silently normalises 004182 to 4182, and the
-- comparison against the user's input then fails for 10% of codes. Questions
Will the string ever start with a zero?
Yes, about 10% of the time, and that is correct behaviour. The draw is uniform over all strings of the requested length, so 0473918205 is exactly as likely as 9473918205. If leading zeros are a problem downstream, the fix is to stop converting the value to a number rather than to bias the generator.
How secure is a six-digit code?
19.93 bits, which is broken as a standalone secret. A six-digit OTP is safe only because it expires quickly and the number of attempts is limited. Remove either of those protections and a million guesses is nothing. That is why NIST SP 800-63B-4 requires rate limiting for any secret below 64 bits.
What is the difference between this and a random number generator?
This draws each character independently from the ten digits, so the result is a fixed-length string uniform over 10 to the power of the length, including values with leading zeros. A number generator draws uniformly from a numeric range, so it has no leading zeros and its length varies. Use this for codes and identifiers, and the number generator for a value in a range.
Why do my codes lose their leading zeros in Excel?
Excel and Google Sheets parse a field of digits as a number on import, which discards leading zeros. Format the column as text before pasting, prefix the value with an apostrophe, or import the CSV through a step that types the column explicitly.
Can I generate a valid card or account number with this?
Not directly. Those formats carry a check digit computed from the preceding digits, usually by the Luhn algorithm, so the last digit is not random. Generate the body here, compute the check digit yourself, and note that it adds no entropy because it is fully determined by the rest.