bcrypt vs SHA-256: Password Hashing Compared
If you’re building authentication, choosing the right hashing algorithm is one of the most important security decisions you’ll make. A poor choice can expose every user’s password within hours of a database breach. This guide provides a deep, technical comparison of SHA-256 and bcrypt, explains why general-purpose hash functions are categorically wrong for password storage, and covers modern alternatives like Argon2id and scrypt.
The Fundamental Problem: Speed Is the Enemy
The core issue is deceptively simple. SHA-256 was designed to be fast — as fast as possible. A modern NVIDIA RTX 4090 GPU can compute over 22 billion SHA-256 hashes per second. This extreme speed is exactly what you want for data integrity verification and digital signatures, but it is catastrophic for password storage.
| Algorithm | Hashes/sec (RTX 4090) | Time to Brute-Force 8-char Password | Time to Brute-Force 12-char Password |
|---|---|---|---|
| MD5 | ~164 billion | Seconds | Hours to days |
| SHA-1 | ~27 billion | Minutes | Days to weeks |
| SHA-256 | ~22 billion | Minutes to hours | Weeks to months |
| bcrypt (cost 10) | ~32 thousand | Decades | Heat death of the universe |
| bcrypt (cost 12) | ~8 thousand | Centuries | Heat death of the universe |
| Argon2id (default) | ~1 thousand | Millennia | Heat death of the universe |
The speed difference is not incremental — it is six orders of magnitude. bcrypt is intentionally, deliberately slow, and that slowness is its greatest security feature.
Understanding Cryptographic Hash Functions vs. Key Derivation Functions
Before comparing specific algorithms, it is critical to understand the distinction between two fundamentally different categories of functions.
Cryptographic Hash Functions (SHA-256, SHA-512, SHA-3)
A cryptographic hash function takes an input of any size and produces a fixed-size output (digest). Its design goals are:
| Property | Meaning | Why It Matters |
|---|---|---|
| Deterministic | Same input always produces the same output | Required for verification |
| Fast computation | Hash can be computed quickly for any input | Needed for file integrity, blockchain, TLS |
| Pre-image resistance | Cannot reverse the hash to find the input | One-way function security |
| Collision resistance | Extremely unlikely for two inputs to produce the same hash | Prevents forged certificates |
| Avalanche effect | Tiny input change produces completely different hash | Prevents pattern analysis |
SHA-256 excels at all of these goals. It is the backbone of TLS certificate chains, Git commit verification, Bitcoin proof-of-work, and file integrity checking. But none of these design goals include resistance to brute-force attacks on short, human-chosen inputs like passwords.
Key Derivation Functions (bcrypt, scrypt, Argon2)
A Key Derivation Function (KDF) is specifically designed to derive a cryptographic key from a low-entropy input (like a human password). Its design goals are fundamentally different:
| Property | Meaning | Why It Matters |
|---|---|---|
| Intentionally slow | Each computation takes a controllable amount of time | Throttles brute-force attacks |
| Built-in salt | Random value mixed into each hash | Prevents rainbow table attacks |
| Configurable cost | Work factor can be increased as hardware improves | Future-proofs against Moore’s Law |
| Memory-hard (Argon2, scrypt) | Requires large amounts of RAM per computation | Resists GPU/ASIC parallelization |
Why SHA-256 Fails for Password Storage
Problem 1: Devastating Speed
SHA-256("password123") → ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
An attacker who obtains this hash from a database breach can use a tool like Hashcat to try billions of candidate passwords per second. The math is straightforward:
- A typical 8-character password using lowercase, uppercase, digits, and symbols has approximately 95^8 ≈ 6.6 quadrillion possible combinations
- At 22 billion SHA-256 hashes per second, exhaustive search takes approximately 3.5 days
- With dictionary attacks and rule-based mutations, most real-world passwords fall in minutes to hours
Problem 2: No Built-in Salt
Without a salt, identical passwords always produce identical hashes:
User Alice: SHA-256("password123") → ef92b778bafe...
User Bob: SHA-256("password123") → ef92b778bafe... (identical!)
This creates two devastating attack vectors:
Rainbow table attacks: An attacker precomputes hashes for millions of common passwords and stores them in a lookup table. When they breach a database, they simply look up each hash. A rainbow table for all 8-character passwords with SHA-256 can be stored in a few terabytes.
Cross-user analysis: If an attacker sees that 500 users have the same hash, they know all 500 users share the same password. Cracking one cracks all of them.
You can add a salt manually before hashing with SHA-256, but this is error-prone. Developers frequently make mistakes like using a static salt for all users, using too-short salts, or storing the salt in a separate location.
Problem 3: No Adaptive Cost Factor
SHA-256 always performs the same computation — there is no way to increase the work required per hash. As GPUs get faster each year, SHA-256 password cracking gets proportionally faster. An algorithm that takes minutes to crack today will take seconds tomorrow.
Generate SHA-256 hashes for data integrity verification (not passwords!) with our Hash Generator.
Why bcrypt Is the Right Choice
bcrypt was designed in 1999 by Niels Provos and David Mazières, specifically for password hashing. It is based on the Blowfish cipher and solves all three problems that SHA-256 has.
Built-in Salt Generation
Every bcrypt hash includes a unique, randomly generated 128-bit salt:
bcrypt("password123", cost=12)
→ $2b$12$LJ3m4ys3Lg5Ey1j3EXAMPLE.HashedOutputHere..............
│ │ └── 22-character salt (Base64-encoded)
│ └── Cost factor (2^12 = 4096 iterations)
└── Algorithm identifier ($2b = bcrypt)
Two users with the same password “password123” will always get different hashes because each call generates a new random salt. This makes rainbow tables completely useless — an attacker would need to build a separate rainbow table for every possible salt value.
Configurable Cost Factor (Key Stretching)
The cost factor (also called work factor or rounds) controls how many iterations of key expansion bcrypt performs internally. Each increment doubles the computation time:
| Cost Factor | Internal Iterations | Approximate Time per Hash | Recommended For |
|---|---|---|---|
| 4 | 16 | ~1ms | Testing/development only |
| 8 | 256 | ~15ms | Not recommended for production |
| 10 | 1,024 | ~65ms | Minimum for production |
| 12 | 4,096 | ~250ms | Recommended for most applications |
| 14 | 16,384 | ~1,000ms | High-security applications |
| 16 | 65,536 | ~4,000ms | Extremely sensitive systems |
The key insight: a 250ms delay is imperceptible during login for a legitimate user. But for an attacker trying to guess billions of passwords, that 250ms per attempt makes brute-force computationally infeasible.
Recommendation: Start with cost 12 and benchmark on your production hardware. The hash should take between 200-500ms. As hardware improves over the years, increase the cost factor by one level.
Resistance to GPU and ASIC Attacks
bcrypt’s key expansion uses the Blowfish cipher, which requires frequent memory accesses to a 4KB state table (the Blowfish S-boxes). GPU architectures are optimized for massively parallel, simple arithmetic — not for memory-intensive operations with data-dependent access patterns. This makes bcrypt approximately 1000x harder to parallelize on GPUs compared to SHA-256.
Modern Alternatives: Argon2id and scrypt
While bcrypt remains excellent, two newer algorithms offer additional protections:
| Feature | bcrypt | scrypt | Argon2id |
|---|---|---|---|
| Year introduced | 1999 | 2009 | 2015 |
| CPU hardness | ✅ Yes | ✅ Yes | ✅ Yes |
| Memory hardness | ❌ No (fixed 4KB) | ✅ Yes (configurable) | ✅ Yes (configurable) |
| Parallelism resistance | ⚠️ Moderate | ✅ Good | ✅ Excellent |
| GPU/ASIC resistance | ⚠️ Moderate | ✅ Good | ✅ Excellent |
| Side-channel resistance | ⚠️ Moderate | ⚠️ Moderate | ✅ Excellent (hybrid mode) |
| Library support | ✅ Universal | ✅ Good | ✅ Growing rapidly |
| Standards body recommendation | Industry standard | NIST mentioned | OWASP recommended |
Argon2id is the winner of the 2015 Password Hashing Competition and is the recommended choice for new projects. It combines:
- Argon2d: Data-dependent memory access (GPU/ASIC resistant but vulnerable to side-channel attacks)
- Argon2i: Data-independent memory access (side-channel resistant but less GPU resistant)
- Argon2id: Hybrid approach — first pass uses data-independent access, subsequent passes use data-dependent access
scrypt is a good intermediate choice when Argon2 libraries are unavailable. It was designed by Colin Percival for the Tarsnap backup service and is memory-hard, but its parameter tuning is less intuitive than Argon2.
Implementation Examples
Node.js with bcrypt
import bcrypt from 'bcrypt';
const COST_FACTOR = 12;
// Hash a password during registration
async function hashPassword(plaintext) {
const hash = await bcrypt.hash(plaintext, COST_FACTOR);
return hash; // Store this in your database
}
// Verify a password during login
async function verifyPassword(plaintext, storedHash) {
const isValid = await bcrypt.compare(plaintext, storedHash);
return isValid; // true if password matches
}
// Example usage
const hash = await hashPassword('MySecureP@ssw0rd!');
console.log(hash);
// $2b$12$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
const isCorrect = await verifyPassword('MySecureP@ssw0rd!', hash);
console.log(isCorrect); // true
Python with bcrypt
import bcrypt
COST_FACTOR = 12
# Hash a password
def hash_password(plaintext: str) -> bytes:
salt = bcrypt.gensalt(rounds=COST_FACTOR)
hashed = bcrypt.hashpw(plaintext.encode('utf-8'), salt)
return hashed
# Verify a password
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
return bcrypt.checkpw(plaintext.encode('utf-8'), stored_hash)
# Example
hashed = hash_password('MySecureP@ssw0rd!')
print(verify_password('MySecureP@ssw0rd!', hashed)) # True
Go with bcrypt
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
const cost = 12
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
return string(bytes), err
}
func verifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
hash, _ := hashPassword("MySecureP@ssw0rd!")
fmt.Println("Hash:", hash)
fmt.Println("Valid:", verifyPassword("MySecureP@ssw0rd!", hash))
}
Node.js with Argon2 (Recommended for New Projects)
import argon2 from 'argon2';
// Hash with Argon2id (recommended variant)
async function hashPassword(plaintext) {
const hash = await argon2.hash(plaintext, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB of RAM
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel threads
});
return hash;
}
// Verify
async function verifyPassword(plaintext, storedHash) {
return await argon2.verify(storedHash, plaintext);
}
Best Practices for Password Hashing in Production
| Practice | Details | Priority |
|---|---|---|
| Never use MD5, SHA-1, or SHA-256 for passwords | They are far too fast and lack built-in salting | Critical |
| Use bcrypt with cost 12+ or Argon2id | Balance security with acceptable login latency | Critical |
| Generate strong passwords | Use our Password Generator to create high-entropy credentials | High |
| Rehash on login when upgrading cost | When you increase the cost factor, hash passwords again as users log in | High |
| Never store plaintext passwords | Obvious, yet database breaches still expose plaintext credentials | Critical |
| Never log passwords | Ensure passwords are never written to application logs, error traces, or monitoring systems | High |
| Implement rate limiting | Limit login attempts to prevent online brute-force attacks (e.g., 5 attempts per minute) | High |
| Use constant-time comparison | Prevent timing attacks when comparing hashes (bcrypt.compare does this automatically) | Medium |
| Pepper your hashes | Add a server-side secret (pepper) before hashing for defense-in-depth | Medium |
The Rehashing Strategy
When you upgrade your cost factor (e.g., from 10 to 12), you cannot rehash existing passwords because you don’t have the plaintext. The solution is to rehash during login:
async function loginUser(email, password) {
const user = await db.findUserByEmail(email);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) return null;
// Check if the hash needs upgrading
const currentCost = parseInt(user.passwordHash.split('$')[2]);
if (currentCost < TARGET_COST) {
const newHash = await bcrypt.hash(password, TARGET_COST);
await db.updatePasswordHash(user.id, newHash);
}
return user;
}
Common Misconceptions Debunked
| Misconception | Reality |
|---|---|
| ”SHA-256 is more secure because it’s newer” | SHA-256 is a better hash function, but being “better” at general hashing makes it worse for passwords — it’s faster |
| ”Adding salt to SHA-256 makes it safe for passwords” | Salt prevents rainbow tables but does nothing against brute-force speed. You still get billions of attempts per second |
| ”bcrypt is too slow for my application” | 250ms per login is imperceptible to users. If your application hashes passwords more than a few times per second, your architecture has other issues |
| ”I can use SHA-256 with many rounds (like PBKDF2)“ | PBKDF2-SHA-256 with 600,000+ iterations is acceptable per OWASP, but Argon2id and bcrypt are still preferred because they resist GPU parallelization |
| ”Password hashing doesn’t matter if my database is secure” | Defense in depth — databases get breached. Every year, major companies lose user data. Proper hashing is your last line of defense |
Further Reading
- Encoding, Hashing, and Encryption: Understanding the Differences
- Password Security: A Complete Developer Guide
- Base64 Is Not Encryption: Understanding the Difference
This article is part of our Encoding and Hashing Guide series. Generate secure passwords with our Password Generator, create bcrypt hashes with our Bcrypt Generator, and compute SHA-256 checksums with our Hash Generator.