SHA-256 vs Bcrypt for Password Hashing
SHA-256 is a great hash function. Bcrypt is a great password hashing function. Those are different jobs, and using the first for the second is one of the most consequential mistakes in production code. This guide explains why fast hashes are wrong for passwords, what bcrypt does differently, when to use Argon2 or PBKDF2 instead, and how to migrate an existing SHA-256 password store to bcrypt without forcing every user to reset. Try the difference yourself in the password hash speed test and use the bcrypt generator to test your verification code.
SHA-256 vs Bcrypt: The Short Answer
Use bcrypt (or Argon2) to store passwords — never SHA-256. SHA-256 is a fast, general-purpose cryptographic hash: a single GPU computes roughly 10 billion of them per second, so a stolen SHA-256 password database is cracked quickly. Bcrypt is deliberately slow (a tunable work factor) and salts every hash automatically, cutting an attacker to a few thousand guesses per second. Same word "hash," opposite design goals.
| Property | SHA-256 | Bcrypt |
|---|---|---|
| Designed for | File integrity, signatures, content addressing | Storing passwords |
| Speed | Fast on purpose (~10B/sec on a GPU) | Slow on purpose (~7k/sec at cost 12) |
| Built-in salt | No | Yes (random, per hash) |
| Tunable work factor | No | Yes (cost parameter) |
| Right for passwords? | No | Yes |
| Modern alternative | — | Argon2id (OWASP's first choice), scrypt, PBKDF2 |
Same Word, Different Jobs
Both SHA-256 and bcrypt are called "hash functions," but they're built for different jobs:
- SHA-256 is a cryptographic hash function — designed for file integrity, digital signatures, and content addressing. It's deterministic (same input always produces the same output), one-way (you can't recover the input from the hash), and fast on purpose. A modern CPU computes millions per second; a GPU computes billions per second.
- Bcrypt is a password hash function (or "key derivation function") — designed specifically for storing passwords. It's deterministic per-salt, one-way, includes built-in random salting, and slow on purpose. A configurable work factor lets you make it slower as hardware gets faster.
Using SHA-256 for passwords is like using a stopwatch as a screwdriver. The stopwatch isn't broken; it's just the wrong tool. The right tool — bcrypt, Argon2, scrypt, or PBKDF2 — is intentionally slow, because every password lookup the defender does once, an attacker with a stolen database has to do billions of times.
The Speed Problem
When an attacker steals your password database, recovery is a guessing game: try a candidate password, hash it, check if it matches a stored hash. The faster the hash function, the more guesses per second:
Hash function Guesses/second on a modern GPU
MD5 100,000,000,000
SHA-256 10,000,000,000
PBKDF2 (600k iterations) 20,000
Bcrypt (cost 12) 7,000
Argon2id (default params) 2,000
Numbers vary by hardware and attack tooling, but the orders of magnitude are stable. Switching from SHA-256 to bcrypt drops the attacker's throughput by a factor of about 1.5 million. A common-password attack that took ten minutes against SHA-256 takes about thirty years against bcrypt. The defender's cost goes from 0.001ms to 250ms per login — invisible to users, devastating to attackers.
You can see the difference directly in the password hash speed test — same input, three algorithms, side-by-side timings on your own device.
What Bcrypt Does That SHA-256 Doesn't
Three structural differences make bcrypt right for passwords and SHA-256 wrong:
- Configurable work factor. Bcrypt's "cost" parameter controls how many rounds of an expensive key-setup phase run before hashing. Each cost increment doubles the time. SHA-256 has no such knob — one round is one round, and there's no built-in way to slow it down.
- Built-in random salt. Every bcrypt hash includes a freshly-generated 16-byte salt embedded in the output. Two users with the same password get two different hashes, defeating precomputed rainbow tables. SHA-256 has no salt by default; you'd have to add one yourself, store it separately, and remember to apply it consistently.
- Single-string storage format. Bcrypt's
$2a$12$salt+hashformat stores everything you need to verify in one column: the algorithm version, the cost factor, the salt, and the hash. SHA-256 setups end up needing separate columns for hash, salt, iteration count, and algorithm — easy to misconfigure, easy to lose track of when migrating.
Code: SHA-256 (Wrong) vs Bcrypt (Right)
Node.js — wrong way (don't do this)
import { createHash } from 'crypto';
function hashPassword(pw) {
return createHash('sha256').update(pw).digest('hex');
}
function verify(pw, storedHash) {
return hashPassword(pw) === storedHash;
}
// Problem: a 10-billion-guess GPU attack against this database
// recovers most passwords in minutes.
Node.js — right way
import bcrypt from 'bcrypt'; // or 'bcryptjs' for pure JS
const COST = 12;
async function hashPassword(pw) {
return bcrypt.hash(pw, COST); // returns "$2b$12$..."
}
async function verify(pw, storedHash) {
return bcrypt.compare(pw, storedHash);
}
// Each verification takes ~250ms on the server.
// An attacker's GPU does ~7,000/sec instead of 10,000,000,000/sec.
Python — wrong way
import hashlib
def hash_password(pw):
return hashlib.sha256(pw.encode()).hexdigest()
def verify(pw, stored):
return hash_password(pw) == stored
# Same problem as the Node version — far too fast.
Python — right way
import bcrypt
COST = 12
def hash_password(pw: str) -> str:
return bcrypt.hashpw(pw.encode(), bcrypt.gensalt(COST)).decode()
def verify(pw: str, stored: str) -> bool:
return bcrypt.checkpw(pw.encode(), stored.encode())
Python — alternative with PBKDF2 (also acceptable)
import hashlib
import os
import secrets
ITERATIONS = 600_000
def hash_password(pw: str) -> str:
salt = secrets.token_bytes(16)
h = hashlib.pbkdf2_hmac('sha256', pw.encode(), salt, ITERATIONS)
# Store algorithm + iterations + salt + hash in one string
return f"pbkdf2_sha256${ITERATIONS}${salt.hex()}${h.hex()}"
def verify(pw: str, stored: str) -> bool:
algo, iters, salt_hex, hash_hex = stored.split('$')
h = hashlib.pbkdf2_hmac('sha256', pw.encode(), bytes.fromhex(salt_hex), int(iters))
return secrets.compare_digest(h.hex(), hash_hex)
Always use a constant-time comparison (secrets.compare_digest in Python, crypto.timingSafeEqual in Node) when checking the result of a password hash. Plain == can leak information through timing.
When SHA-256 IS Appropriate
SHA-256 is the right tool for many things — just not passwords:
- File integrity — verify a downloaded file matches a published hash.
- Digital signatures — sign a document, anyone with your public key can verify the signature.
- Content-addressable storage — Git commit IDs, IPFS, Docker image layers all use SHA-256 (or similar) to give every piece of content a unique address.
- HMAC — keyed message authentication for API request signing, JWT signatures, webhook verification.
- Inside PBKDF2 — PBKDF2-HMAC-SHA256 is a perfectly fine password hash because the iteration count provides the slowness; SHA-256 is just the underlying primitive.
The hash generator on this site gives you SHA-256, SHA-1, SHA-512, and MD5 for these use cases. None of those should ever be used directly for passwords.
Migrating from SHA-256 to Bcrypt
If you have a production database of SHA-256 password hashes, you don't need to force every user to reset. Three migration patterns work:
1. Lazy migration on next login
The most common approach. On every successful login, while you have the plaintext password in memory, check whether the stored hash is in the old format. If yes, compute the new bcrypt hash and update the row. After a few weeks of normal traffic, most active users have been migrated. Inactive users stay on the old hash until they log in.
# Python pseudocode
def authenticate(username, password):
user = db.get_user(username)
if not user:
return False
if user.hash_format == 'sha256':
# Old hash — verify with old method
if hashlib.sha256(password.encode()).hexdigest() != user.password_hash:
return False
# Re-hash with bcrypt and store the new hash
user.password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(12)).decode()
user.hash_format = 'bcrypt'
db.save(user)
return True
# New hash — verify with bcrypt
return bcrypt.checkpw(password.encode(), user.password_hash.encode())
2. Wrap the old hash inside bcrypt
If you can't wait for users to log in (e.g., you're shutting down the old infrastructure soon), you can migrate every row in a batch by hashing the existing SHA-256 hash with bcrypt: store bcrypt(sha256(password)). Verification then becomes "first SHA-256 the input, then bcrypt-compare." This works without ever needing the plaintext, but commits you to that wrapping permanently.
3. Force resets for inactive accounts
After lazy migration has been running for a few months, set a deadline. Any account still on the old hash gets a "your password needs to be reset" email. After the deadline, accounts that haven't migrated lose access until they reset. Reasonable for security-sensitive applications; user-hostile for low-stakes ones.
Summary
- SHA-256 is fast and deterministic — perfect for file integrity, signatures, and HMAC. Wrong for passwords.
- Bcrypt is slow and salted — perfect for passwords. Cost factor 12 is the modern default.
- Argon2id is the latest and strongest password hash; bcrypt and PBKDF2 are also fine modern choices.
- Never store plain SHA-256 of a password. PBKDF2-SHA256 with 600,000+ iterations is OK if you need a SHA-based path.
- Use lazy migration to move existing SHA-256 stores to bcrypt without forcing resets.
- Use a constant-time comparison when checking hashes to avoid timing-attack leaks.
Try It Yourself
The password hash speed test runs SHA-256, PBKDF2, and bcrypt against the same input on your device — the timing difference makes the security argument visceral. Use the bcrypt generator to test your verification code with hashes you control. The hash generator covers SHA-256 and friends for non-password use cases (file integrity, signatures, content addressing).
Frequently Asked Questions
Is SHA-256 ever safe for password hashing?
A single round of SHA-256 is never safe for passwords — it's far too fast. SHA-256 is safe and appropriate for file integrity checking, digital signatures, content-addressable storage, HMAC, and similar non-password contexts. The problem with passwords is specifically that an attacker who steals your hash database needs to be slowed down to a few thousand guesses per second instead of billions. If you must use SHA-256 for password hashing because that's what your stack supports, use it as the primitive inside PBKDF2 with at least 600,000 iterations — that's the OWASP 2023 recommendation and is supported by the browser's Web Crypto API.
How much faster is SHA-256 than bcrypt in practice?
On a single CPU core, SHA-256 takes about 1 microsecond per hash; bcrypt at cost 12 takes about 250 milliseconds. That's a 250,000× difference. On a modern GPU optimized for SHA-256, an attacker can do 10 billion hashes per second. The same GPU running bcrypt at cost 12 manages a few thousand per second. The exact ratio matters less than the order of magnitude — SHA-256 lets attackers test every password in a leaked rockyou.txt list against every account in your database in minutes. Bcrypt lets them test maybe a hundred passwords per account in the time you have to detect the breach and force resets.
How do I migrate from SHA-256 to bcrypt without forcing password resets?
The standard pattern: on every successful login (when you have the plaintext password in memory anyway), check whether the stored hash is in the old format. If yes, compute the new bcrypt hash and update the row. After a few weeks every active user has been migrated. For inactive accounts, you can either leave them on the old hash with a flag that forces a password reset on next login, or batch-migrate by wrapping the old SHA-256 hash inside bcrypt: store bcrypt(sha256(password)) and adjust your verification code to do the inner SHA-256 first. The wrapping approach lets you migrate every record in a single migration script without ever needing the plaintext, but commits you to that wrapping forever.
Should I use bcrypt, Argon2, scrypt, or PBKDF2?
All four are acceptable modern choices and any of them is dramatically better than fast SHA-256. Argon2id (winner of the 2015 Password Hashing Competition) is the strongest because it's memory-hard, which makes GPU and ASIC attacks much more expensive. Bcrypt is the most widely supported, has been deployed for over two decades, and remains a perfectly safe choice for most applications. Scrypt is also memory-hard but less common today. PBKDF2 is FIPS-approved and built into the browser's Web Crypto API and most standard libraries, but it's not memory-hard. Pick the one your framework makes easiest; the bigger decision is to pick any modern adaptive hash over single-round SHA-256 or MD5.
Why does every bcrypt hash for the same password look different?
Bcrypt automatically generates a fresh random salt for every hash and embeds it inside the 60-character output. Hashing the password "password123" twice produces two different strings — but both verify against "password123" when checked. The salt is the second part of the hash, after the cost factor. Because each user gets a unique salt, an attacker can't precompute a single rainbow table and reuse it across your whole database — they have to attack each password individually, multiplying the work by the number of accounts. SHA-256 has no built-in salt, which is part of why it's wrong for passwords; if you use SHA-256 directly, identical passwords produce identical hashes, and an attacker who cracks one account has cracked everyone with that password.