Random letter string generator

Upper and lowercase letters, no digits and no symbols. Fifty-two characters, 5.700440 bits each.

G
More options
up to 10,000

Constraints

Fifty-two characters, and one exact identity

log2(52) is 5.700440, which is exactly log2(26) plus 1. Fifty-two is twice 26, so the second case buys precisely one bit per character. Twelve letters carry 68.41 bits, which clears the 64-bit floor NIST SP 800-63B-4 sets for a secret that has to stand without rate limiting. Fifteen carry 85.51 and twenty-three carry 131.11.

Against a 62-character alphanumeric alphabet, letters-only costs 0.254 bits per character. Over twelve characters that is 3.05 bits, which is less than one extra character. If a field rejects digits, this is a cheap constraint to satisfy rather than a compromise worth arguing about.

Letters-only lengths against the alternatives
CharactersLetters only (52)Uppercase only (26)Alphanumeric (62)
845.60 bits37.60 bits47.63 bits
1268.41 bits56.41 bits71.45 bits
1691.21 bits75.21 bits95.27 bits
23131.11 bits108.11 bits136.95 bits

Case sensitivity is the whole risk

A letters-only string spends every one of its characters on the distinction between upper and lower case. That makes it the alphabet most exposed to case folding, because folding does not remove a few characters from the pool, it halves it.

MySQL ships a case-insensitive collation by default, so a twelve-character letter token stored in an ordinary varchar column and compared with equals matches regardless of case. The alphabet is effectively 26, the entropy per character drops by exactly one bit, and the value falls from 68.41 bits to 56.41. Nothing in the application changes and no error is raised. The same applies to DNS labels, HTTP header field names, Windows file names, the default macOS APFS configuration, and any comparison written with a case-insensitive collation or a lowercasing normalisation step.

The fix is to know which it is before you choose the alphabet. If the storage or comparison path folds case, generate from an uppercase-only alphabet and read the honest 4.700440 bits per character, or make the column binary and keep the fifty-two.

The case-folding loss for a letters-only string is exactly one bit per character, because 52 is exactly twice 26. No other common alphabet has such a clean answer.

Letters-only strings can spell things

Every string of a given length is equally likely, and some of them are words. At twelve characters the chance of an accidental English word is negligible, but short codes are a different matter, and a substring can be a problem even when the whole string is not. For anything mass-printed, put on a physical product, or shown to customers, this is a business risk rather than a security one.

The citable mitigation is the nolookalikesSafe alphabet from nanoid-dictionary, which is vowel-free: 6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz, thirty-six characters at 5.169925 bits each. Without vowels the output cannot form an ordinary word. It is available as a preset on the custom character set page.

When letters-only is genuinely the right constraint

Fields validated against a letters-only pattern, which is common in legacy systems and in forms that were written before anyone considered generated values. Identifiers in generated code, XML and HTML id attributes and CSS class names, all of which have rules about not beginning with a digit and are simpler to satisfy by excluding digits entirely.

Values that must not be reinterpreted as numbers. A letters-only string cannot be parsed as a number by a spreadsheet import, a loosely typed JSON consumer or a shell arithmetic expansion, and it cannot lose leading zeros because it has none. Sort keys where mixed numeric and lexical ordering would be confusing fall in the same category.

Doing this in your own code

JavaScript
// Uniform over the alphabet, with no modulo bias.
// The alphabet is spread into code points first, so an emoji or an
// accented character counts as one symbol rather than two UTF-16 units.
function randomString(length, alphabetStr) {
  const alphabet = [...new Set([...alphabetStr])];
  const n = alphabet.length;
  if (n < 1) throw new RangeError("empty alphabet");

  const limit = 4294967296 - (4294967296 % n);   // largest multiple of n <= 2^32
  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 += alphabet[x % n];
  }
  return out;
}

randomString(16, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");

The do/while is the whole point. Dropping it in favour of a bare x % n reintroduces modulo bias.

Python 3.6+
import secrets

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

"".join(secrets.choice(ALPHABET) for _ in range(16))

# secrets.choice is backed by secrets.randbelow, which rejects and redraws,
# so it is uniform. random.choice is not a secure source and must not be used.

Iterating a Python str yields code points, so non-ASCII alphabets work without extra handling.

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

func RandomString(length int, alphabet string) (string, error) {
    runes := []rune(alphabet)
    n := big.NewInt(int64(len(runes)))
    out := make([]rune, length)
    for i := range out {
        j, err := rand.Int(rand.Reader, n)   // rejection sampled internally
        if err != nil {
            return "", err
        }
        out[i] = runes[j.Int64()]
    }
    return string(out), nil
}

Convert to []rune, not []byte. Indexing a Go string yields bytes, which splits any multi-byte character.

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

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

fn random_string(length: usize) -> String {
    // rand::rng() is a thread-local ChaCha12 CSPRNG seeded from the OS.
    let mut rng = rand::rng();
    (0..length)
        .map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char)
        .collect()
}

random_range rejection-samples, so it is unbiased. On rand 0.8 the method is named gen_range and the constructor is rand::thread_rng().

PHP 7+
<?php
function random_string(int $length, string $alphabet): string {
    $n = strlen($alphabet);
    $out = '';
    for ($i = 0; $i < $length; $i++) {
        $out .= $alphabet[random_int(0, $n - 1)];
    }
    return $out;
}

echo random_string(16, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');

random_int throws on entropy failure and is unbiased. rand and mt_rand are neither.

Java 8+
import java.security.SecureRandom;

static String randomString(int length, String alphabet) {
    SecureRandom rng = new SecureRandom();
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
        sb.append(alphabet.charAt(rng.nextInt(alphabet.length())));
    }
    return sb.toString();
}

nextInt(bound) rejects and redraws, so it is uniform. Reuse one SecureRandom instance rather than constructing one per call.

C# / .NET
using System.Security.Cryptography;

const string Alphabet =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

// .NET 8 and later
string s = RandomNumberGenerator.GetString(Alphabet, 16);

// Earlier versions, same distribution
var sb = new System.Text.StringBuilder(16);
for (int i = 0; i < 16; i++)
    sb.Append(Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)]);
Shell
# 16 alphanumeric characters from the kernel CSPRNG.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16; echo

# tr -dc discards every byte outside the set rather than folding it in,
# which makes it a rejection sampler. Each of the 62 bytes has exactly one
# preimage, so the output is uniform.

# openssl, when you want bytes rather than a character set
openssl rand -hex 16
openssl rand -base64 24

Questions

How many bits are in twelve random letters?

68.41 bits, from 12 times log2(52). That clears the 64-bit level NIST SP 800-63B-4 treats as the floor for a secret needing no rate limiting, provided nothing downstream folds the case.

What happens if my database ignores case?

The alphabet collapses from 52 to 26 and you lose exactly one bit per character. A twelve-character token drops from 68.41 bits to 56.41, silently. MySQL default collations, DNS labels, HTTP header names, Windows file names and default macOS APFS all fold case. Use a binary collation, or generate from an uppercase-only alphabet and read the honest number.

Why would I exclude digits?

Usually because something downstream requires it: a validation pattern that only accepts letters, an identifier that cannot begin with a digit, or a field that must not be reinterpreted as a number by a spreadsheet or a loosely typed parser. The entropy cost against a full alphanumeric alphabet is 0.254 bits per character.

Can the generator produce an offensive string?

It can, because every string of the requested length is equally likely. If that is a real risk, use the vowel-free nolookalikesSafe alphabet on the custom character set page, which cannot form ordinary words at 5.169925 bits per character.

Is a letters-only string safe to put in a URL?

Yes, without encoding. RFC 3986 lists both letter cases as unreserved characters. The caveat is that a URL path is case-sensitive while the host portion is not, so a letters-only token belongs in the path or the query string rather than in a subdomain.

Related generators