How to Create Strong Passwords: A Developer's Guide

Passwords have been declared dead every few years since at least 2004, when Bill Gates predicted biometrics would replace them. In 2026, passwords remain the dominant authentication mechanism for the vast majority of systems. Understanding what makes a password strong — and why — is still one of the most practically useful things a developer can know.

This guide covers the theory (entropy, attack models) and the practice (what to build, what to recommend to users, how to store credentials correctly).

What Makes a Password Strong?

A strong password is one that is computationally infeasible to guess within the attacker’s available time and resources. That definition depends on three variables: the password itself, the attack method, and the computational resources available to the attacker.

In practical terms, a strong password has three properties:

Length

Length is the single most important factor. Every additional character multiplies the search space exponentially. A 16-character password from a modest alphabet is dramatically harder to crack than an 8-character password from a larger one.

Most modern guidance (NIST SP 800-63B, 2024 revision) recommends a minimum of 12 characters for user-chosen passwords and 16+ characters for anything generated by a machine. NIST has moved away from mandatory complexity rules (requiring uppercase, numbers, symbols) because research showed they produced predictable patterns (Password1!) without meaningfully increasing entropy.

Character Diversity

A password drawn from a larger alphabet is stronger than one drawn from a smaller alphabet of the same length. The four character classes are:

  • Lowercase letters: 26 characters
  • Uppercase letters: 26 characters
  • Digits: 10 characters
  • Symbols: approximately 32 printable ASCII symbols

Using all four classes gives an alphabet size of approximately 94. A 12-character password from this alphabet has 94^12 ≈ 4.76 × 10^23 possible values. A 12-character lowercase-only password has 26^12 ≈ 9.54 × 10^16 — about 5 million times smaller.

However, character diversity only matters if the characters are distributed uniformly throughout the password. A password that has the required uppercase letter at position 1 and the required digit at position 11 is far weaker than it appears because attackers know about this pattern.

Randomness

The most overlooked property. A password is only as strong as the process that generated it. A 20-character string that follows a predictable pattern (a phrase from a song, a word with letter substitutions, a keyboard walk) is far weaker than its length suggests.

True strength comes from cryptographic randomness — characters drawn independently from a uniform distribution using a cryptographically secure pseudorandom number generator (CSPRNG). This is what separates a machine-generated password from a human-chosen one.

Common Password Mistakes

Understanding attack patterns reveals why certain password habits are catastrophically weak.

Dictionary Words and Variations

Attackers do not start from pure brute force. They start from wordlists. The rockyou.txt dataset — leaked in 2009 and still used as a baseline — contains 14 million real passwords. Extended wordlists used in modern attacks contain hundreds of millions of entries, including:

  • Dictionary words in every major language
  • Common names, cities, sports teams
  • Leet-speak substitutions (p@ssw0rd, s3cur1ty)
  • Appended numbers and years (password2024, qwerty123)
  • Capitalized first letters (Welcome1, Dragon9)

A password like Tr0ub4dor&3 — famously used in the XKCD 936 comic to illustrate password strength — is memorable but crackable. Its apparent complexity is undermined by the fact that it follows patterns (word, substitutions, appended special character) that are well-represented in modern rulesets.

Personal Information

Passwords derived from names, birthdays, phone numbers, addresses, or pet names are vulnerable to targeted attacks. An attacker who knows their target can build a personalized wordlist from public social media in minutes. This category of attack is especially relevant for high-value targets (executives, administrators, public figures).

Even partial personal information is dangerous. A password like JohnSmith1985! can be cracked immediately once an attacker knows the target’s name and approximate age.

Password Reuse

Reuse is the most dangerous pattern in practice. When any service is breached and passwords are exposed, attackers immediately test those credentials against other services in a technique called credential stuffing. The scale of this problem is documented by HaveIBeenPwned, which tracks over 14 billion breached accounts.

A single reused password can cascade across every service a person uses. For developers, this means a breached development tool login can expose production systems if credentials are shared.

Predictable Patterns and Keyboard Walks

Keyboard adjacency patterns (qwerty, asdfgh, 1qaz2wsx) are enumerated in every modern attack dictionary. Sequential patterns (abc123, 111111, aaaaaa) are typically tested first. Position-based patterns — symbols at the end, uppercase at the start — are incorporated into rule-based attacks that expand wordlists by applying transformations.

Password Entropy Explained

Entropy is the information-theoretic measure of unpredictability. For passwords, it quantifies the number of guesses required to brute-force a password from a given character set.

The formula is straightforward:

H = L × log2(N)

Where:

  • H = entropy in bits
  • L = password length (number of characters)
  • N = size of the character set (alphabet)

Worked Examples

8-character lowercase password:

H = 8 × log2(26) = 8 × 4.70 = 37.6 bits

At 10 billion guesses per second (a modern GPU cluster), this is cracked in about 5 hours.

12-character full ASCII password:

H = 12 × log2(94) = 12 × 6.55 = 78.6 bits

At the same rate, this would take approximately 1.2 million years to brute-force exhaustively.

20-character full ASCII password:

H = 20 × log2(94) = 20 × 6.55 = 131 bits

Beyond any realistic attack — the number of guesses required exceeds the total number of atoms in the observable universe.

Entropy Targets

Use CaseMinimum EntropyExample
Low-risk web accounts40 bits8-char full ASCII
Standard user accounts60 bits10-char full ASCII
High-value accounts80 bits12-char full ASCII
API keys / tokens128 bits20-char full ASCII
Encryption keys256 bitsDedicated key generation

NIST SP 800-63B (2024) recommends not expressing password requirements in terms of entropy to users, but the underlying math is essential for developers setting policy and building generators.

Why Human-Chosen Passwords Have Lower Effective Entropy

The formula above assumes uniform random selection from the full character set. Human-chosen passwords do not satisfy this condition. People gravitate toward certain letters (e and t appear far more than q and z in English text), certain positions (uppercase at the start, digits at the end), and entire categories of strings (words, dates, names).

Research by Bonneau and others has estimated the effective entropy of user-chosen passwords at 10-20 bits for most real-world policies — far below what the character set calculation would suggest. This is why machine generation with a CSPRNG is categorically superior to user invention for high-security contexts.

Best Practices for Developers

Password Storage: Never Store Plaintext

Passwords must be stored as hashes, never as plaintext or reversible encryption. If your database is breached, hashed passwords give users time to change their credentials before they can be used.

Use a memory-hard password hashing function, not a general-purpose cryptographic hash:

bcrypt — the established standard, deliberately slow, with a configurable work factor:

import bcrypt from "bcrypt";

const SALT_ROUNDS = 12; // ~250ms on modern hardware

// Hashing (at registration)
async function hashPassword(plaintext: string): Promise<string> {
  return bcrypt.hash(plaintext, SALT_ROUNDS);
}

// Verification (at login)
async function verifyPassword(
  plaintext: string,
  hash: string
): Promise<boolean> {
  return bcrypt.compare(plaintext, hash);
}

Argon2id — the winner of the 2015 Password Hashing Competition, the current recommended choice for new systems. It is resistant to both GPU attacks (via memory-hardness) and side-channel attacks (the “id” variant):

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(
    time_cost=2,       # iterations
    memory_cost=65536, # 64 MB
    parallelism=2,
)

# Hashing
hash = ph.hash("user_password_here")

# Verification
try:
    ph.verify(hash, "user_password_here")
    valid = True
except VerifyMismatchError:
    valid = False

Do not use MD5, SHA-1, SHA-256, or any general-purpose hash for passwords. These are fast by design, which makes them easy to brute-force. A modern GPU can compute 10+ billion SHA-256 hashes per second.

Enforce Minimum Length, Not Complexity Rules

NIST’s updated guidance recommends:

  • Minimum 8 characters for user-chosen passwords (12 is better)
  • Maximum at least 64 characters (do not cap at 20 or 30)
  • No mandatory complexity rules (they produce predictable patterns)
  • Check against known-breached passwords using the HaveIBeenPwned API
// Check a password against the HIBP Pwned Passwords API
// Uses k-anonymity: only the first 5 chars of the SHA-1 hash are sent
async function isPwned(password: string): Promise<boolean> {
  const encoder = new TextEncoder();
  const data = encoder.encode(password);
  const hashBuffer = await crypto.subtle.digest("SHA-1", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();

  const prefix = hashHex.slice(0, 5);
  const suffix = hashHex.slice(5);

  const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const text = await response.text();

  return text.split("\n").some((line) => line.startsWith(suffix));
}

Multi-Factor Authentication

A strong password is necessary but not sufficient. Two-factor authentication (2FA) ensures that a compromised password alone cannot grant access. For developer-facing systems:

  • TOTP (Time-based One-Time Passwords, RFC 6238) via apps like Authy or Google Authenticator is the baseline. Libraries: otpauth (JS), pyotp (Python).
  • WebAuthn / Passkeys are the next step — cryptographic authentication that eliminates passwords entirely for supported clients.
  • Hardware keys (YubiKey, FIDO2) for highest-security contexts.

For internal developer tooling, SSO with an identity provider (Okta, Auth0, Entra ID) that enforces 2FA is preferable to rolling your own authentication.

Password Managers

Recommend password managers to users and use them yourself. The major options — 1Password, Bitwarden (open source), Dashlane — all generate, store, and autofill strong random passwords. A password manager enables a different model: every account gets a unique, machine-generated, 20+ character password, and the user remembers only the manager’s master password.

For teams, shared vault features in 1Password Teams or Bitwarden Organizations allow credential sharing without actually sharing the plaintext password.

How Password Generators Work

The critical implementation detail in a password generator is the source of randomness.

The Right Way: crypto.getRandomValues

The Web Crypto API’s crypto.getRandomValues fills a typed array with cryptographically strong random values from the operating system’s entropy pool (/dev/urandom on Linux/macOS, CryptGenRandom on Windows):

function generatePassword(
  length: number,
  charset: string
): string {
  const chars = charset.split("");
  const randomValues = new Uint32Array(length);
  crypto.getRandomValues(randomValues);

  return Array.from(randomValues)
    .map((val) => chars[val % chars.length])
    .join("");
}

// Usage
const FULL_CHARSET =
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";

const password = generatePassword(16, FULL_CHARSET);

Note the modulo bias issue: val % chars.length is not perfectly uniform when chars.length does not evenly divide 2^32. For typical charset sizes (26, 52, 94 characters), the bias is negligible — less than 0.003%. For cryptographic key generation (where you need exact uniformity), use rejection sampling:

function generatePasswordUnbiased(
  length: number,
  charset: string
): string {
  const chars = charset.split("");
  const result: string[] = [];
  const maxValid = Math.floor(0x100000000 / chars.length) * chars.length;

  while (result.length < length) {
    const randomValues = new Uint32Array(length * 2);
    crypto.getRandomValues(randomValues);
    for (const val of randomValues) {
      if (result.length >= length) break;
      if (val < maxValid) {
        result.push(chars[val % chars.length]);
      }
    }
  }

  return result.join("");
}

The Wrong Way: Math.random

Math.random is a pseudorandom number generator seeded from the current time. It is not cryptographically secure — its internal state can be inferred from observed outputs. Do not use it for security-sensitive values:

// NEVER do this for passwords:
function weakPassword(length: number): string {
  const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  return Array.from({ length }, () =>
    chars[Math.floor(Math.random() * chars.length)]
  ).join("");
}

The same applies to Python’s random module (use secrets instead), PHP’s rand() (use random_int()), and any language’s non-cryptographic RNG.

Server-Side Generation

For server-side password generation, use the platform’s CSPRNG directly:

import secrets
import string

def generate_password(length: int = 16) -> str:
    alphabet = string.ascii_letters + string.digits + string.punctuation
    return "".join(secrets.choice(alphabet) for _ in range(length))

# The secrets module uses os.urandom() internally
package main

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

func generatePassword(length int, charset string) (string, error) {
    result := make([]byte, length)
    for i := range result {
        n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        if err != nil {
            return "", err
        }
        result[i] = charset[n.Int64()]
    }
    return string(result), nil
}

Generate secure passwords instantly with our Password Generator tool.

FAQ

How long should a password be in 2026?

For user-chosen passwords, 12 characters is a reasonable minimum. For machine-generated passwords (API keys, initial credentials), 16-20 characters is standard. For anything resembling a cryptographic secret (session tokens, signing keys), generate at least 128 bits (22 base64url characters or 32 hex characters) using a dedicated token generation function, not a password generator.

Is a passphrase better than a random password?

A long passphrase drawn from a large wordlist (Diceware using the EFF word list, for example) can have comparable entropy to a shorter random password while being more memorable. A 6-word Diceware passphrase has entropy of 6 × log2(7776) ≈ 77.5 bits — roughly equivalent to a 12-character full-ASCII password. The trade-off is length: passphrases are typically 25-40 characters, which matters for systems with character limits. For systems that humans must type regularly, passphrases are often the better choice.

What should I do if a service limits passwords to 8-10 characters?

This is a red flag that the service may be storing passwords incorrectly (plaintext or in a field with a small column size). Use a unique password for this service regardless of the limit, report the issue to their security team, and monitor HaveIBeenPwned for breaches of that service.

Does changing passwords regularly still make sense?

Current guidance from NIST and NCSC has moved away from mandatory periodic rotation for accounts without evidence of compromise. Forced rotation leads to predictable patterns (incrementing numbers, seasonal variations) that weaken security. The new guidance: use a strong, unique password and change it when there is reason to believe it was compromised.

What is the difference between a password and an API key?

A password is a human-memorable secret used for interactive authentication. An API key is a machine-readable bearer token used for programmatic access. API keys should be generated with at least 128 bits of entropy, stored in secrets management systems (AWS Secrets Manager, HashiCorp Vault, environment variables injected at runtime), rotated automatically, and scoped to the minimum required permissions. Never commit API keys to source control — use tools like git-secrets or GitHub’s push protection to prevent accidental exposure.

How do I handle password reset securely?

Generate a one-time reset token using a CSPRNG with at least 128 bits of entropy. Store a hash of the token (not the token itself) in your database. Set an expiry of 15-60 minutes. Invalidate the token immediately after use. Send the token via email as a URL parameter and never log it. The reset flow should not reveal whether an email address has an account (to prevent user enumeration).