Engineering

🧂 Salt, Pepper and Stretching: Password Hashing Dev Guide 2026

By Alex Chen, Random Password Tool · 18 June 2026 · 7 min read · 1,550 words

Salted password hashing implementation guide requests usually boil down to one question: how do I store a password so that a database breach does not become an account-takeover catastrophe? Salted password hashing answers that. It means running each user's password through a one-way, deliberately slow hash function (bcrypt or Argon2id) together with a unique random value called a salt. The salt guarantees that two users with the same password produce completely different hashes, which defeats precomputed rainbow tables and forces attackers to crack every hash individually. You never store the plaintext—only the salt and the resulting digest. This guide shows you exactly how to do it.

If you have ever written md5(password) or sha256(password) and shipped it, this article is your remediation plan. We will cover salts, peppers, stretching, work factor tuning, and production-ready code in both Python and Node.js, aligned with NIST SP 800-63B and OWASP guidance.

What Is Salt in Password Hashing?

A salt is a unique, cryptographically random value generated per password and stored alongside the hash. Before hashing, the salt is concatenated with (or fed into) the password input, so identical passwords yield different digests.

Why does this matter? Without a salt, an attacker who steals your database can compare your stored hashes against a precomputed table mapping common passwords to their hashes—a rainbow table. One lookup cracks every account that used hunter2. With a unique 16-byte salt per user, rainbow tables become useless: the attacker would need a separate table for every salt value, which is computationally infeasible.

Key properties of a correct salt:

Good news: bcrypt and Argon2id generate and embed the salt for you. The encoded hash string contains the algorithm, parameters, salt, and digest in a single self-describing field. You do not manage salts manually.

Why Salt Alone Is Not Enough (Pepper)

Salt defeats precomputation, but it does nothing if an attacker has both your database and enough GPU time to brute-force weak passwords one salt at a time. This is where a pepper adds a second layer.

A pepper is a secret value—identical for all users—that is not stored in the database. It lives in a secrets manager, an environment variable, or an HSM. Two common implementations:

The threat model is specific: a pepper protects you when an attacker exfiltrates the database but not the application secrets. If they breach your config store too, the pepper provides no extra protection—so treat it as defense-in-depth, not a substitute for a strong slow hash. Both OWASP and NCSC describe pepper as an optional supplementary control. CISA's secure-by-design guidance similarly emphasizes layering: salt is mandatory, pepper is a bonus, and key rotation matters more than people assume.

Work Factor and Stretching

Salting stops precomputation; stretching stops speed. Stretching (also called key stretching) means making the hash function deliberately expensive—thousands of internal iterations, large memory allocation, or both—so that each guess costs the attacker measurable time and hardware.

The tunable that controls this cost is the work factor. In bcrypt it is the cost parameter (a base-2 exponent for the number of rounds). In Argon2id it is a triple: time cost, memory cost, and parallelism. As CPUs and GPUs get faster, you increase these values. A hash that took 250ms in 2020 might take 40ms today—meaning attackers crack it 6x faster—so password hashing best practices 2026 require periodic retuning.

The calibration rule of thumb: target roughly 250ms–500ms per hash on your production hardware for interactive logins. Fast enough that users don't notice; slow enough that mass brute-forcing is uneconomical. Benchmark on the box that actually serves auth, not your laptop.

bcrypt vs Argon2id Implementation

bcrypt (1999, Blowfish-based) is battle-tested, ubiquitous, and has one notable quirk: it silently truncates input at 72 bytes. Argon2id (winner of the 2015 Password Hashing Competition) is the modern recommendation from OWASP and NIST because it is memory-hard—resistant to GPU and ASIC cracking in a way bcrypt is not. For a full head-to-head, read our companion piece: Argon2 vs bcrypt: a 2026 developer guide.

bcrypt in Python

import bcrypt

# Hashing on signup
def hash_password(password: str) -> bytes:
    # cost=12 generates the salt automatically
    salt = bcrypt.gensalt(rounds=12)
    return bcrypt.hashpw(password.encode("utf-8"), salt)

# Verifying on login
def verify_password(password: str, stored_hash: bytes) -> bool:
    return bcrypt.checkpw(password.encode("utf-8"), stored_hash)

bcrypt in Node.js

const bcrypt = require("bcrypt");

// Hashing on signup
async function hashPassword(password) {
  const saltRounds = 12; // work factor
  return bcrypt.hash(password, saltRounds);
}

// Verifying on login
async function verifyPassword(password, storedHash) {
  return bcrypt.compare(password, storedHash);
}

Argon2id in Python

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(
    time_cost=3,       # iterations
    memory_cost=65536, # 64 MiB in KiB
    parallelism=4,     # lanes / threads
    hash_len=32,
    salt_len=16,
)

def hash_password(password: str) -> str:
    return ph.hash(password)  # salt generated and embedded automatically

def verify_password(stored_hash: str, password: str) -> bool:
    try:
        ph.verify(stored_hash, password)
        if ph.check_needs_rehash(stored_hash):
            # parameters increased since this hash was created
            pass  # re-hash and persist the new value
        return True
    except VerifyMismatchError:
        return False

Argon2id in Node.js

const argon2 = require("argon2");

async function hashPassword(password) {
  return argon2.hash(password, {
    type: argon2.argon2id,
    timeCost: 3,
    memoryCost: 65536, // 64 MiB
    parallelism: 4,
  });
}

async function verifyPassword(storedHash, password) {
  return argon2.verify(storedHash, password);
}

Key Configuration Parameters

The two algorithms expose different knobs. The table below summarizes sane production starting points for 2026; always benchmark and adjust toward your 250–500ms target.

ParameterbcryptArgon2id
Primary work factorcost = 12 (approx 2^12 rounds)time_cost = 3 iterations
Memory hardnessNone (fixed 4 KiB)memory_cost = 65536 KiB (64 MiB)
ParallelismNot configurableparallelism = 4 lanes
SaltAuto, 16 bytes, embeddedAuto, 16 bytes, embedded
Output length60-char string32-byte digest (configurable)
Input length limit72 bytes (truncated!)Effectively unlimited
GPU/ASIC resistanceModerateStrong (memory-hard)
OWASP 2026 stanceAcceptable for legacyRecommended default

OWASP's current minimum for Argon2id is m=19456 (19 MiB), t=2, p=1; the 64 MiB / t=3 configuration above is a stronger, server-grade baseline. For bcrypt, a cost of 10 is the floor and 12 is the recommended starting point for 2026.

Common Implementation Mistakes

If you are scripting credential generation or rotation from a terminal, our guide to Linux command-line password generation pairs well with the hashing patterns here. And for managing the secrets and peppers your application depends on, a dedicated secrets workflow like the 1Password CLI lets you inject peppers and database credentials at runtime without committing them to source control.

Frequently Asked Questions

Is salting still necessary if I use bcrypt or Argon2id?

Yes—but you do not implement it manually. Both algorithms automatically generate a unique random salt per password and embed it in the output string. Salting is mandatory and built in; you simply must not disable or override it with a static value.

Should I use a pepper in addition to a salt?

A pepper is an optional defense-in-depth layer. It helps only when an attacker steals your database but not your application secrets. Store the pepper outside the database (secrets manager or HSM). It is recommended by OWASP and NCSC as a supplement, never a replacement for a salted slow hash.

bcrypt or Argon2id—which should I choose in 2026?

Choose Argon2id for new projects: it is memory-hard and resistant to GPU/ASIC cracking, and it is the OWASP and NIST-aligned default. bcrypt remains acceptable for existing systems, but mind its 72-byte input limit. See our full Argon2 vs bcrypt comparison for the detailed trade-offs.

How do I choose the right work factor?

Benchmark on your production hardware and target roughly 250–500ms per hash for interactive logins. For bcrypt start at cost 12; for Argon2id start at t=3, 64 MiB memory, p=4. Re-measure annually and increase the parameters as hardware improves.

What does NIST SP 800-63B say about password storage?

NIST SP 800-63B requires that passwords (“memorized secrets”) be salted and hashed with a suitable one-way function using a cost factor, allow at least 64 characters and all printable Unicode, and avoid arbitrary composition rules. It also recommends checking new passwords against known-breached lists.

This page contains affiliate links. If you purchase through these links, we may earn a commission at no extra cost to you.

Generate a Free Strong Password →

More Password Security Tools

🔑 SecureKeyGen⚔️ TitanPasswords🛡️ Best Password Generator🔐 Free Strong Password⚡ Instant Password🗝️ Iron Vault Keys👨‍👩‍👧‍👦 Safe Pass Builder🛡️ Trusty Password⚙️ StrongPassFactory🔑 SecureKeyGen.org📚 TrustyPassword.org
We use cookies to improve your experience. Learn more