To create a strong password, let a cryptographically secure random generator pick it, make it at least 15 characters long, use it for one account only, and keep it in a password manager. Each of those four steps has a measurable reason behind it, and this guide works through them: how many bits a random password carries, how fast a graphics card can guess against a stolen hash, what the current NIST guideline asks of websites, and where human-made passwords fail.
The bit counts and probabilities below come from the code of the ZeroTool password generator and are recomputed by the site’s test suite. Breach counts were looked up on 2026-10-02 and will have changed since.
Start from a random generator, not a pattern
A password is strong when an attacker has to try many guesses before reaching it. Attackers do not try strings in alphabetical order. They start with passwords from earlier breaches and with common words, then apply rules such as capitalizing the first letter, swapping a for @ and adding 1! at the end. A password built from those habits is near the front of the queue however complicated it looks.
The Have I Been Pwned Pwned Passwords service lists how often a password appears in breach data. On 2026-10-02:
| Password | Times seen in breaches |
|---|---|
P@ssw0rd | 6,421,042 |
p@ssw0rd | 941,966 |
Password1! | 584,516 |
P@ssw0rd1! | 40,649 |
correcthorsebatterystaple | 4,173 |
Tr0ub4dor&3 | 3,196 |
P@ssw0rd1! satisfies a typical rule of upper case, lower case, digit and symbol, and it has still been seen tens of thousands of times. The last two rows are the examples from xkcd 936. Once an example is published, people copy it, so it ends up in the lists too. The fix is to take the choice away from people: if every character is drawn at random, the attacker has no pattern to start from.
How many bits a random password has
When each of L characters is drawn independently and uniformly from a set of N characters, there are NL possible passwords, and the strength in bits is L × log2N. Each extra bit doubles the number of guesses needed to try them all. The formula only applies to passwords that were generated this way; it says nothing about a password a person chose.
| Character set | N | bits per character | 12 chars | 16 chars | 20 chars |
|---|---|---|---|---|---|
| lowercase | 26 | 4.70 | 56.4 | 75.2 | 94.0 |
| lowercase + digits | 36 | 5.17 | 62.0 | 82.7 | 103.4 |
| letters + digits | 62 | 5.95 | 71.5 | 95.3 | 119.1 |
| ZeroTool, all four sets, look-alikes excluded | 83 | 6.38 | 76.5 | 102.0 | 127.5 |
| ZeroTool, all four sets | 88 | 6.46 | 77.5 | 103.4 | 129.2 |
| all 94 printable ASCII except space | 94 | 6.55 | 78.7 | 104.9 | 131.1 |
Length does more than the character set. Going from 62 to 88 characters adds about half a bit per character; adding four characters from the same 62-character set adds about 24 bits. A 20-character password of letters and digits (119.1 bits) is stronger than a 16-character password that also uses symbols (103.4 bits).
How long offline guessing takes
Online guessing against a login form is slow because the site can limit attempts. The dangerous case is offline guessing: the attacker has stolen the stored password hashes and tests guesses on their own hardware, as fast as the hash function allows. A public hashcat 6.2.6 benchmark of a single RTX 4090 gives these rates:
| Stored as | Guesses per second (one RTX 4090) |
|---|---|
| NTLM | 288.5 billion |
| MD5 | 164.1 billion |
| SHA-1 | 50.6 billion |
| PBKDF2-HMAC-SHA256, 999 iterations | 8.87 million |
| bcrypt, cost 5 (32 iterations) | 184,000 |
| scrypt, N = 16384 | 7,126 |
The time to try every random password from the 88-character set on that one card:
| Length | Unsalted MD5 | bcrypt cost 5 |
|---|---|---|
| 8 | 6.1 hours | 619 years |
| 10 | 5.4 years | 4.8 million years |
| 12 | 41,600 years | 37 billion years |
Eight lowercase letters stored as MD5 fall in 1.3 seconds. The table assumes one card and a fully random password; a cracking rig with eight cards is eight times faster, and a password with a human pattern falls far sooner than the table suggests. You cannot choose how a website stores your password, which is why the length you control matters. bcrypt’s cost doubles the work at each step, so the cost 10 that many frameworks use is 32 times slower to attack than the cost 5 benchmarked here.
What NIST SP 800-63B says about length and rules
NIST SP 800-63B (revision 4, published in 2025) is the US guideline for how services handle passwords. Its rules for verifiers, the services that check your password, read as a checklist for both sides:
- A password used as the only factor must be at least 15 characters. If it is one part of multi-factor authentication, it may be shorter but must be at least 8.
- Services should allow at least 64 characters, accept all printing ASCII characters and the space, and accept Unicode, counting each code point as one character.
- Services “SHALL NOT impose other composition rules (e.g., requiring mixtures of different character types)”. The guideline’s appendix explains why: a user who would have picked
passwordtends to pickPassword1when forced to add a capital and a digit. - Services must not require periodic password changes, but must force a change when there is evidence of compromise.
- New passwords must be compared against a blocklist of commonly used, expected or compromised values, such as breach corpuses, dictionary words and the service name.
- Services must allow password managers and autofill, and should let you paste a password.
For a person, the practical reading is: 15 characters or more, random, unique, stored in a manager. If a site still enforces composition rules, see the last section for how to meet them with a generator.
Passphrases and Diceware
A passphrase made of random words is easier to type on a phone and easier to remember when you must, such as the master password of a password manager. The math is the same as for characters, with a word list as the set. Arnold Reinhold’s Diceware list, first published in 1995, has 7,776 words, one for each result of five six-sided dice (65 = 7,776). The EFF published new lists of the same size in 2016 with more familiar words.
Each word chosen by dice from a 7,776-word list is worth log27776 = 12.9 bits. Four words give 51.7 bits, five give 64.6 and six give 77.5, about the same as 12 random characters from the 88-character set.
The words must be chosen by dice or a secure random generator. A phrase you invent, such as a song lyric or a quote, is not random, and correcthorsebatterystaple shows that a famous phrase is no better than P@ssw0rd.
Generating a password with Python or JavaScript
In Python, the secrets module draws from the operating system’s secure random source and secrets.choice picks uniformly:
import secrets
import string
alphabet = string.ascii_letters + string.digits + string.punctuation # 94 characters
password = "".join(secrets.choice(alphabet) for _ in range(20))
print(len(alphabet), len(password))
In JavaScript, Math.random() is not a secure source. Use crypto.getRandomValues(), and do not reduce the values with % alone: when the set size does not divide the range of the random values, the first characters of the set come up more often. With random bytes and the 88-character set, 256 % 88 = 80, so 80 characters have a 3-in-256 chance per position and the other 8 have 2 in 256. The ZeroTool generator draws 32-bit values and rejects any value at or above the largest multiple of the set size, then draws again. The same method, as a function you can paste:
function randomPassword(length, charset) {
const n = charset.length;
const limit = 2 ** 32 - (2 ** 32 % n); // largest multiple of n below 2^32
const buf = new Uint32Array(length);
let out = '';
while (out.length < length) {
crypto.getRandomValues(buf);
for (const v of buf) {
if (v < limit && out.length < length) out += charset[v % n];
}
}
return out;
}
console.log(randomPassword(20, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'));
For machine secrets such as session keys or API signing keys, skip the character set and take random bytes: openssl rand -base64 32 or Python’s secrets.token_urlsafe(32) both give 256 bits.
Checking a password against breach data
The Pwned Passwords range API lets you check a password without sending it. You send the first five hexadecimal characters of its SHA-1 hash; the service returns every hash suffix in its data that starts with that prefix, with a count, and you look for your suffix locally. This is called k-anonymity. The Add-Padding: true header adds random dummy entries so the response size does not reveal much either. For P@ssw0rd, the SHA-1 hash starts with 21BD1, and the padded response for that prefix had about two thousand lines.
import hashlib
import urllib.request
def pwned_count(password: str) -> int:
digest = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
prefix, suffix = digest[:5], digest[5:]
req = urllib.request.Request(
f"https://api.pwnedpasswords.com/range/{prefix}",
headers={"Add-Padding": "true", "User-Agent": "password-check-example"},
)
with urllib.request.urlopen(req) as resp:
for line in resp.read().decode().splitlines():
hash_suffix, count = line.split(":")
if hash_suffix == suffix:
return int(count)
return 0
A count of zero does not make a password good; it only means this exact string is not in that data set. A password from a random generator will almost always return zero, and that is the expected result.
Storing passwords if you run the login
If you build the service, the guessing speeds above are why the storage method matters. NIST SP 800-63B requires passwords to be salted and hashed with a password hashing scheme, a salt of at least 32 bits, and a cost factor “as high as practical”, raised over time. RFC 9106 gives two recommended Argon2id settings: t=1, p=4 with 2 GiB of memory, or, when that much memory is not available, t=3, p=4 with 64 MiB. bcrypt is still widespread; it only uses the first 72 bytes of the password, which matters once you allow the 64 characters NIST asks for, because one Japanese or Chinese character takes three bytes in UTF-8. NIST also says the verifier must check the entire submitted password and not truncate it.
Using the ZeroTool password generator
The password generator runs in the browser with crypto.getRandomValues(), and the page loads no analytics or ad scripts. The settings map to the tables above:
- Length from 4 to 128, default 20.
- Character sets: upper case (26), lower case (26), digits (10) and 26 symbols
!@#$%^&*()-_=+[]{}|;:,.<>?. The quote characters, backslash, slash, backtick and tilde are not in the list. With all four sets the generator draws from 88 characters. - Exclude ambiguous removes
0 O l 1 I, leaving 83 characters, for passwords someone has to read off a screen. - Strength label: the bar shows length × log2(set size), labelled Very Weak below 40 bits, Weak below 60, Fair below 80, Strong below 100 and Very Strong from 100. The default of 20 characters from 88 shows Very Strong (129 bits).
- Generate 10 fills a box with ten passwords at once.
Every character is drawn independently from the whole set, so a password is not guaranteed to contain every type. For 20 characters from the 88-character set, the chance that there is no digit is 9.0%, and the chance that at least one of the four types is missing is 9.2%. At 8 characters it is 51.6%. If a site insists on one of each type, press Generate again; that keeps the result random, while editing a character by hand puts a pattern back in.
If a site rejects some symbols, untick Symbols and add two or three characters of length to make up the difference: 22 characters from the 62-character set give 131.0 bits, more than the 129.2 bits of 20 characters with symbols.