How to Hash Passwords Securely: bcrypt, Argon2 and scrypt Compared
Every year, data breaches expose billions of credentials. When attackers get access to a database, the first thing they look for is the users table. If passwords are stored in plain text, every account is instantly compromised. Even if passwords are hashed with a fast algorithm like SHA-256, modern GPUs can test billions of candidates per second. Password hashing algorithms like bcrypt, Argon2 and scrypt exist specifically to make this kind of brute-force attack impractical.
Why You Should Never Store Plain-Text Passwords
Storing passwords in plain text means that a single database leak exposes every user's credentials immediately. Attackers do not even need to run any computation. They can log in as any user, and because most people reuse passwords across services, a breach on your site leads to credential stuffing attacks on banking, email and social media accounts.
High-profile breaches at companies like LinkedIn, Adobe and RockYou all exposed millions of poorly protected passwords. The lesson is clear: passwords must be hashed before storage, and the hash function must be specifically designed for passwords.
Regular Hashing vs Password Hashing
General-purpose hash functions like SHA-256 and MD5 are designed to be fast. That speed is a feature when you are verifying file integrity or building hash tables. But for password storage, speed is the enemy. A single NVIDIA RTX 4090 can compute over 20 billion MD5 hashes per second and over 8 billion SHA-256 hashes per second. An attacker with a few GPUs can test every possible 8-character password in hours.
There are two additional problems with using regular hash functions for passwords:
- Rainbow tables. Attackers precompute hashes for millions of common passwords and dictionary words. If you hash
password123with SHA-256, the output is always the same. An attacker just looks it up in a table. - No salt. Without a unique random value (salt) prepended to each password before hashing, identical passwords produce identical hashes. An attacker can spot every user who chose
123456in a single glance.
What Makes a Password Hash Function Secure
A proper password hashing function has three properties that general-purpose hash functions lack:
- Slow by design. The function is deliberately expensive to compute. A cost factor lets you tune how slow it is. As hardware gets faster, you increase the cost factor to maintain the same resistance.
- Built-in salting. The function automatically generates a unique random salt for each password and stores it alongside the hash. Two users with the same password get completely different hashes.
- Memory-hard (modern algorithms). Algorithms like Argon2 and scrypt require large amounts of RAM to compute. This makes GPU and ASIC attacks far more expensive because those devices have limited memory per core.
bcrypt Explained
bcrypt was published in 1999 and is based on the Blowfish cipher. It has been the industry standard for password hashing for over two decades. bcrypt accepts a cost factor (also called work factor or rounds) that controls how many iterations of the internal key derivation are performed. Each increment of the cost factor doubles the computation time.
A bcrypt hash looks like this:
$2b$12$LJ3m4ys3Lg2VhkOYbQKm0eDzBJHgMO.6ShFkHleJCeyMfg2DXKmTi
Breaking this down: $2b$ is the algorithm identifier, 12 is the cost factor (2^12 = 4096 iterations), and the remaining characters contain the 22-character salt followed by the 31-character hash, both encoded in a custom base64 format.
bcrypt in Node.js
import bcrypt from "bcryptjs"; // Hash a password const password = "my-secret-password"; const saltRounds = 12; const hash = await bcrypt.hash(password, saltRounds); // "$2b$12$LJ3m4ys3Lg2VhkOYbQKm0e..." // Verify a password const isValid = await bcrypt.compare(password, hash); // true
bcrypt in Python
import bcrypt # Hash a password password = b"my-secret-password" salt = bcrypt.gensalt(rounds=12) hashed = bcrypt.hashpw(password, salt) # Verify a password is_valid = bcrypt.checkpw(password, hashed) # True
A cost factor of 12 is a reasonable default in 2026. On modern hardware it takes roughly 250ms to hash a single password, which is imperceptible to a user logging in but devastating to an attacker trying billions of guesses. One important limitation: bcrypt truncates passwords at 72 bytes. If your application accepts very long passphrases, consider pre-hashing with SHA-256 before passing the result to bcrypt.
Argon2 Explained
Argon2 won the Password Hashing Competition (PHC) in 2015 and is widely considered the most advanced password hashing algorithm available. It comes in three variants:
- Argon2d. Maximizes resistance against GPU cracking but is vulnerable to side-channel attacks.
- Argon2i. Optimized for resistance against side-channel attacks, suitable for environments where timing attacks are a concern.
- Argon2id. A hybrid that provides both GPU resistance and side-channel resistance. This is the recommended variant for password hashing.
Argon2 has three configurable parameters: memory cost (how much RAM is required), time cost (number of iterations), and parallelism (number of threads). The memory-hardness is what sets Argon2 apart. By requiring, say, 64 MB of RAM per hash computation, it becomes extremely expensive to run on GPUs where each core has very limited memory.
Argon2 in Node.js
import argon2 from "argon2";
// Hash a password with Argon2id
const password = "my-secret-password";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 4,
});
// "$argon2id$v=19$m=65536,t=3,p=4$..."
// Verify a password
const isValid = await argon2.verify(hash, password);
// trueArgon2 in Python
from argon2 import PasswordHasher
ph = PasswordHasher(
memory_cost=65536, # 64 MB
time_cost=3,
parallelism=4,
)
# Hash a password
hash = ph.hash("my-secret-password")
# Verify a password
try:
ph.verify(hash, "my-secret-password") # Returns True
except Exception:
pass # Invalid passwordThe OWASP recommendation for Argon2id is a minimum of 19 MB memory, a time cost of 2, and 1 degree of parallelism. For higher security, 64 MB with a time cost of 3 provides strong protection while keeping login latency under 500ms on typical server hardware.
scrypt Explained
scrypt was designed in 2009 by Colin Percival, originally for the Tarsnap backup service. Like Argon2, scrypt is memory-hard. It requires a configurable amount of RAM, making it resistant to hardware-accelerated attacks. scrypt is parameterized by three values:
- N (CPU/memory cost). Must be a power of 2. Higher values require more memory and computation. A common value is 2^15 (32768).
- r (block size). Controls the size of each block of memory. Typically set to 8.
- p (parallelism). Number of parallel threads. Usually set to 1 for password hashing.
scrypt is a solid choice and is used in production by systems like Django (as an alternative hasher) and several cryptocurrency protocols. However, it predates the Password Hashing Competition and its parameter tuning is less intuitive than Argon2. If you are starting a new project, Argon2id is generally preferred. If you are already using scrypt with well-tuned parameters, there is no urgent reason to migrate.
scrypt in Node.js
import { scrypt, randomBytes } from "node:crypto";
import { promisify } from "node:util";
const scryptAsync = promisify(scrypt);
// Hash a password
const password = "my-secret-password";
const salt = randomBytes(16).toString("hex");
const derivedKey = await scryptAsync(password, salt, 64, {
N: 32768,
r: 8,
p: 1,
});
const hash = salt + ":" + derivedKey.toString("hex");
// Verify: split stored hash, re-derive, compareComparison: bcrypt vs Argon2 vs scrypt
| Feature | bcrypt | Argon2id | scrypt |
|---|---|---|---|
| Year introduced | 1999 | 2015 | 2009 |
| Memory-hard | No | Yes | Yes |
| Built-in salt | Yes | Yes | Manual |
| Side-channel resistant | Yes | Yes (Argon2id) | Partial |
| Max password length | 72 bytes | Unlimited | Unlimited |
| Recommendation | Safe fallback | First choice | Good alternative |
Which One Should You Use in 2026?
If you are starting a new project, use Argon2id. It is the most modern algorithm, it won the Password Hashing Competition, it is memory-hard, and it provides the most tunable security parameters. OWASP, NIST, and the majority of security researchers recommend it as the default choice for new applications.
If Argon2 is not available in your framework or language ecosystem, bcrypt remains an excellent and battle-tested fallback. It has over 25 years of real-world usage and no practical attacks have been found against it when used with a sufficient cost factor (12 or higher).
If you are already using scrypt with proper parameters, there is no need to rush a migration. scrypt is still considered secure. However, for new projects, Argon2id is preferred because of its cleaner API, better parameter model, and formal recognition from the PHC.
Common Mistakes to Avoid
- Using MD5 or SHA-256 for passwords. These are general-purpose hash functions designed for speed. They are not suitable for password storage. A GPU can compute billions of SHA-256 hashes per second.
- Setting the cost factor too low. A bcrypt cost of 4 or an Argon2 memory cost of 1 MB provides almost no protection. Benchmark on your production hardware and target 200-500ms per hash.
- Not using unique salts. If you use a single global salt (or no salt at all), identical passwords produce identical hashes. Always use the library's built-in salt generation. Never roll your own.
- Hashing passwords client-side only. If you hash on the client and send the hash to the server, the hash itself becomes the password. An attacker who steals the database can send the stolen hashes directly. Password hashing must happen server-side.
- Not upgrading cost factors over time. Hardware gets faster every year. What took 250ms in 2020 might take 50ms in 2026. Re-hash passwords with a higher cost factor when users log in.
- Implementing your own password hashing. Use well-maintained libraries. Never write your own bcrypt or Argon2 implementation. Cryptographic code is extremely difficult to get right.
Password Hashing vs General-Purpose Hashing
It is worth clarifying the distinction. Password hashing algorithms (bcrypt, Argon2, scrypt) are server-side libraries designed for one specific job: making stored passwords resistant to brute-force attacks. General-purpose hash functions (SHA-256, MD5, SHA-512) are used for data integrity, digital signatures, checksums, and many other purposes. Both types are called “hashing,” but they serve fundamentally different roles.
The Hash Generator on codetools.run lets you experiment with standard hash functions like SHA-256, MD5 and SHA-512 directly in your browser. These are useful for understanding how hashing works, verifying checksums, and generating digests for non-password use cases. For actual password storage, you will need the server-side libraries described in this article.
Explore hash functions interactively
See how SHA-256, MD5 and other algorithms work with our free Hash Generator tool. Generate and compare hashes for any input instantly in your browser.
Open Hash Generator