Generate 100 random strings

One hundred random strings in a single pass, with the duplicate probability for your exact configuration shown alongside them.

G
More options
up to 10,000

Constraints

A hundred is a boundary, not a limit

One hundred is a strangely specific thing to search for, and there is a reason it gets searched. It is the exact ceiling on four of the fifteen generators we audited on page one: randomkeygen, generate-random.org, uuidtools and commentpicker all stop there. miniwebtool stops at 50. jam.dev and browserling have no quantity field at all and return one string per click. For a lot of people, 100 is the number at which the tool they were already using ran out.

Two of those four generate on a server, and their limit is really two limits. generate-random.org and uuidtools both return HTTP 429 at the sixty-first request inside a minute, verified by probe, so the practical ceiling is 100 per request and 60 requests per minute, with every value produced on a machine you do not control before it reaches you.

On this page 100 is a default. Change the quantity and the same code path produces 10 or 10,000, chunked with a progress bar and a cancel button, so a larger request becomes a wait you can watch and stop rather than a frozen tab. The engine, the alphabet controls and the export formats are the same ones the main generator uses; only the starting quantity differs.

Will 100 strings all be different?

At the default here, 12 characters from a 62-character alphabet, each string carries 71.45 bits and there are about 3.23 x 10^21 possibilities. The chance that any two of your hundred match is roughly 1.5 x 10^-18, about one in 650 quadrillion. Switching on the unique option changes nothing you could observe in a lifetime of pressing the button.

Shorten the string and that stops being true quickly, because the birthday bound scales with the square of the batch size against the size of the space. A hundred lowercase strings of three letters draw from only 17,576 possibilities, and about 24.5 percent of those batches contain a duplicate. That is one batch in four, from a configuration that looks perfectly reasonable on screen. Six digits, the shape of a typical one-time code, gives a million possibilities and a duplicate in about one batch in 200.

So the tool shows the number instead of a reassuring word. The probability for your current length, alphabet and quantity sits beside the output and updates when you change any of them. None of the fifteen publishes collision maths for the configuration you are actually using, including gigacalculator, which does deduplicate correctly but never says what the odds were.

Chance that a batch of 100 contains at least one duplicate
ConfigurationPossible valuesChance of a duplicate in 100
Lowercase letters, 3 characters17,57624.5 percent
Digits only, 6 characters1,000,0000.49 percent
62 characters, 4 long14,776,3360.033 percent
62 characters, 6 long5.68 x 10^108.7 x 10^-8
62 characters, 8 long2.18 x 10^142.3 x 10^-11
62 characters, 12 long3.23 x 10^211.5 x 10^-18

A duplicate in a short batch is not evidence of a weak random source. It is the expected behaviour of any correct generator drawing with replacement from a small space, and the only fixes are a longer string, a larger alphabet, or deduplication.

Guaranteed unique, and when that is impossible

The unique option deduplicates across the entire batch rather than inside a chunk, which is the distinction that matters once a batch is generated in pieces. A value that has already been emitted is rejected and redrawn, so a hundred strings means a hundred distinct strings.

That guarantee has a hard limit, and the tool checks for it before it starts rather than discovering it inside a loop. Two characters over the 62-character alphabet gives 3,844 distinct strings, so a request for 10,000 unique strings of that shape is not slow, it is impossible. The check compares the requested count against the size of the space, refuses with the real number, and names the three settings that would make the request possible, leaving whatever was on screen where it was.

Below that limit the cost still rises as you approach the size of the space, because a redraw becomes likelier with every string already accepted. At half the space you average 1.39 draws per accepted string; at 99 percent of it, 4.65. For a hundred strings of any sensible length this is invisible, but it is worth knowing before you ask for most of a small space.

What a hundred strings are usually for

The quantity is nearly always a round hundred because this is a fixtures job: a hundred test users, a hundred coupon codes for a campaign, a hundred device identifiers to seed a staging database, a hundred keys to load-test an endpoint against. In each case the format the strings arrive in matters as much as the strings, because they are going somewhere specific.

The same hundred values come out as plain lines, a JSON array, JSON objects with an index, quoted CSV, TSV, ready-to-paste SQL INSERT rows, .env lines, a shell array, a Python list or a JavaScript array literal, and download as .txt, .csv or .json. None of the fifteen offers a structured format, and miniwebtool puts its CSV and JSON downloads behind a rewarded video advert or a paid plan.

One caution about the CSV route, which applies to every tool that offers one. Excel and Google Sheets evaluate a cell whose value begins with =, + or -, and Excel does the same for a leading @, so an unescaped CSV of generated values is a formula-injection vector against whoever opens it. With the alphanumeric default here that cannot occur; enable symbols or paste a custom character set and it can. The export quotes every field, prefixes an affected value with a single apostrophe, and tells you it did. And if the strings are keys, treat the downloaded file as live secret material and delete it once it has been loaded.

Where the hundred come from

Every character is selected with crypto.getRandomValues, the browser interface to the operating system CSPRNG, using rejection sampling rather than a modulo so that no character in the alphabet is more likely than another. Five of the fifteen generators we audited use Math.random in their generator logic, which is a fast non-cryptographic PRNG whose internal state can be recovered from a short run of its own output.

For a batch the entropy is drawn once into a pooled buffer instead of once per character, which is why a hundred strings appear without a perceptible pause. Nothing is transmitted at any point, and there is no request to inspect, which you can confirm by opening the network panel before you press the button.

Doing this in your own code

JavaScript
// One pooled draw for the whole batch, not one syscall per character.
// crypto.getRandomValues has a 65,536-byte quota per call, so the pool
// is refilled in chunks rather than sized to the request.
function bulkStrings(count, length, alphabet) {
  const n = alphabet.length;
  const limit = Math.floor(2 ** 32 / n) * n;  // rejection threshold
  const pool = new Uint32Array(4096);
  let i = pool.length;                        // forces a refill on first use

  const draw = () => {
    for (;;) {
      if (i === pool.length) { crypto.getRandomValues(pool); i = 0; }
      const v = pool[i++];
      if (v < limit) return v % n;            // discard the biased tail
    }
  };

  const out = new Array(count);
  for (let s = 0; s < count; s++) {
    let str = "";
    for (let c = 0; c < length; c++) str += alphabet[draw()];
    out[s] = str;
  }
  pool.fill(0);                               // leftover words are key material
  return out;
}

Without the limit check, v % n is modulo biased, because 2 ** 32 is not a multiple of 62.

JavaScript, non-blocking
// Yield to the browser between chunks. This is what makes a progress bar
// paint and a cancel button work: both are checked between chunks, so
// they take effect within one frame instead of after the whole batch.
async function bulkChunked(count, length, alphabet, onProgress, signal) {
  const out = [];
  const CHUNK = 2000;
  while (out.length < count) {
    if (signal && signal.aborted) throw new DOMException("cancelled", "AbortError");
    const n = Math.min(CHUNK, count - out.length);
    for (const s of bulkStrings(n, length, alphabet)) out.push(s);
    onProgress(out.length / count);
    await new Promise(requestAnimationFrame);
  }
  return out;
}

A generator that runs one synchronous loop cannot offer a cancel button, because nothing else can run while the loop is running.

Python 3.6+
import secrets, string

ALPHABET = string.ascii_letters + string.digits   # 62 characters

def bulk(count, length=16):
    return ["".join(secrets.choice(ALPHABET) for _ in range(length))
            for _ in range(count)]

# Unique across the batch, with the feasibility check FIRST.
def bulk_unique(count, length=16):
    space = len(ALPHABET) ** length
    if count > space:
        raise ValueError(
            f"only {space:,} distinct strings exist at length {length}"
        )
    out = set()
    while len(out) < count:
        out.add("".join(secrets.choice(ALPHABET) for _ in range(length)))
    return out

Skip the check and the while loop never returns when the request exceeds the size of the space. secrets.choice rejection samples internally. random.choice rejection samples too, but it runs on the Mersenne Twister and is not cryptographically secure.

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

const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

func bulk(count, length int) ([]string, error) {
    out := make([]string, count)
    max := big.NewInt(int64(len(alphabet)))
    for i := range out {
        b := make([]byte, length)
        for j := range b {
            n, err := rand.Int(rand.Reader, max)
            if err != nil {
                return nil, err
            }
            b[j] = alphabet[n.Int64()]
        }
        out[i] = string(b)
    }
    return out, nil
}

crypto/rand.Int rejection samples for you. math/rand is a different package with a different guarantee and must not be substituted.

Rust
// rand = "0.9"
use rand::Rng;

const ALPHABET: &[u8] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

fn bulk(count: usize, length: usize) -> Vec<String> {
    let mut rng = rand::rng();
    (0..count)
        .map(|_| {
            (0..length)
                .map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char)
                .collect()
        })
        .collect()
}

random_range rejection-samples, so there is nothing to fix here. rand::rng() is a ChaCha12 CSPRNG seeded from the operating system. On rand 0.8 the two names are rand::thread_rng() and gen_range.

Shell
# 100 strings of 16 characters, straight from the kernel CSPRNG.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | fold -w 16 | head -n 100

# Unique across the batch, with the count checked afterwards.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | fold -w 16 | head -n 200 \
  | sort -u | head -n 100 | wc -l

# If you write them to disk, overwrite and unlink afterwards. shred's own
# manual cautions that it assumes the filesystem and hardware overwrite in
# place, which many do not, and that backups may still hold a copy.
shred -u tokens.txt

LC_ALL=C is load bearing. In a multibyte locale tr can reject input bytes as invalid and the pipeline stalls.

Export formats
Plain lines
  kQ2mZ8vRt4XbN1pW
  7yLcF0jHsDe5Ma3U

JSON array
  ["kQ2mZ8vRt4XbN1pW", "7yLcF0jHsDe5Ma3U"]

.env lines
  API_KEY_1=kQ2mZ8vRt4XbN1pW
  API_KEY_2=7yLcF0jHsDe5Ma3U

SQL INSERT
  INSERT INTO api_keys (token) VALUES
    ('kQ2mZ8vRt4XbN1pW'),
    ('7yLcF0jHsDe5Ma3U');

CSV, every field quoted
  index,value
  1,"kQ2mZ8vRt4XbN1pW"
  2,"7yLcF0jHsDe5Ma3U"

In the CSV export a value beginning with =, +, - or @ is additionally prefixed with a single apostrophe, so a spreadsheet treats it as text rather than a formula.

Questions

Can I generate more than 100 at once?

Yes. One hundred is only the starting value of the quantity field on this page. The same generator runs to 10,000, chunked with a progress bar and a cancel button, and the ceiling is stated on screen rather than applied silently.

Are the 100 strings guaranteed unique?

They are if you switch the unique option on, which deduplicates across the whole batch. Left off, uniqueness is probabilistic: at the 12-character default a duplicate has a probability of about 1.5 x 10^-18, but for three lowercase letters about 24.5 percent of hundred-string batches contain one.

Why did my short codes collide when the generator is correct?

Because the birthday bound applies to any correct generator drawing with replacement. With 17,576 possible three-letter codes, a hundred draws collide about a quarter of the time no matter how good the entropy is. Lengthen the code, widen the alphabet, or turn on deduplication.

How do I get the strings into a spreadsheet or a database?

Use the format switcher above the output. Quoted CSV and TSV paste straight into a spreadsheet, SQL INSERT rows paste straight into a client, and there are JSON array, JSON object, .env and language-literal forms as well. All of them download as a file, and none is behind a gate.

What length should I use for coupon or referral codes?

Long enough that the collision table above reads as a number you are comfortable with at the volume you plan to issue, and short enough to be typed. Eight characters over 62 gives 2.18 x 10^14 possibilities and a 1 percent duplicate chance only after about 2.09 million codes. If the codes will be read aloud, exclude the ambiguous characters too, and accept that it costs a little entropy.

Related generators