If you’re storing user passwords with AES, RSA, or even SHA-256, you’re doing it wrong. Not slightly wrong — fundamentally wrong. This is the single most common security mistake in web development, and it has a straightforward fix: use a proper password hashing function like bcrypt.
Here’s why it matters, how bcrypt works, and what production-ready code looks like.
Why Passwords Need Special Treatment
Most cryptographic operations are designed to be reversible or fast. Encryption is reversible by design — that’s its entire purpose. SHA-256 and MD5 are fast, processing gigabytes per second. Both of those properties are catastrophic for passwords.
When an attacker gets your database, they get your password hashes. With encryption, if they find the key, they decrypt everything. With fast hashes like MD5 or SHA-256, they run a GPU-accelerated brute-force attack — modern hardware can test hundreds of billions of MD5 hashes per second. Your “complex” password is cracked in minutes.
Passwords need to be:
- Not reversible — even with the key or algorithm, you can’t get the plaintext back
- Slow to compute — deliberate slowness makes brute force attacks impractical
- Unique per user — identical passwords must produce different hashes
bcrypt satisfies all three. General-purpose hash functions and encryption do not.
Hashing vs Encryption vs Password Hashing
These are not interchangeable:
| Approach | Reversible? | Fast? | Safe for Passwords? |
|---|---|---|---|
| Encryption (AES, RSA) | Yes — with key | Yes | No |
| Fast hashing (MD5, SHA-256) | No | Yes (by design) | No |
| Password hashing (bcrypt, Argon2id) | No | No (by design) | Yes |
The reversibility of encryption is a deal-breaker: compromise the key and you compromise every password. Fast hashing is a deal-breaker too: speed enables brute force. Password hashing functions are engineered to be slow — and that’s the point.
How bcrypt Works
bcrypt, designed in 1999 by Niels Provos and David Mazières, does three things that matter:
1. Salting. Before hashing, bcrypt generates a random salt (16 bytes) and includes it in the hash output. Even if two users share the same password, their hashes are different. This defeats precomputed rainbow table attacks entirely.
2. Work factor (cost). bcrypt accepts a cost parameter (typically 10–14). Each increment doubles the computation time. At cost 12, hashing takes roughly 250–400ms on modern hardware. That’s imperceptibly slow for a login request — but it turns a billion-attempt brute force into a decades-long operation.
3. The output is self-contained. A bcrypt hash looks like $2b$12$... and encodes the algorithm version, cost factor, salt, and hash together. You don’t need a separate salt column. Store the whole string.
Production Code: Node.js and Python
Node.js (bcryptjs or bcrypt)
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// Hash a password
async function hashPassword(plaintext) {
return bcrypt.hash(plaintext, SALT_ROUNDS);
}
// Verify a password against a stored hash
async function verifyPassword(plaintext, storedHash) {
return bcrypt.compare(plaintext, storedHash);
}
// Usage
const hash = await hashPassword('hunter2');
// Store `hash` in your database
const isValid = await verifyPassword('hunter2', hash);
// true
Python (bcrypt)
import bcrypt
COST = 12
def hash_password(plaintext: str) -> bytes:
salt = bcrypt.gensalt(rounds=COST)
return bcrypt.hashpw(plaintext.encode('utf-8'), salt)
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
return bcrypt.checkpw(plaintext.encode('utf-8'), stored_hash)
# Usage
hashed = hash_password('hunter2')
# Store hashed in your database
is_valid = verify_password('hunter2', hashed)
# True
Store the full hash string. Never store plaintext, never store the salt separately, never store the intermediate values.
Choosing a Work Factor
The right cost factor depends on your hardware. The goal: make each hash operation take 200–500ms on your production server. That’s fast enough for good UX, slow enough to frustrate attackers.
Current recommendation: cost 12 as a minimum, 14 for high-value accounts (admin, financial). Run a benchmark on your actual hardware:
// Node.js: benchmark different cost factors
const bcrypt = require('bcrypt');
for (let cost = 10; cost <= 14; cost++) {
const start = Date.now();
await bcrypt.hash('benchmark', cost);
console.log(`Cost ${cost}: ${Date.now() - start}ms`);
}
If cost 12 takes under 100ms, increase it. If cost 14 takes over 1000ms, drop to 13. Revisit annually — hardware gets faster, and your cost factor should keep pace.
You can test bcrypt hashing interactively with the IO Tools bcrypt Hash Generator.
bcrypt vs Argon2id vs scrypt
bcrypt is battle-tested and widely supported. But it has a limitation: it’s not memory-hard. An attacker with specialized hardware (ASICs or FPGAs) can parallelize attacks more efficiently than with memory-hard alternatives.
| Algorithm | Memory-Hard | Parallelism Resistant | Recommendation |
|---|---|---|---|
| bcrypt | No | Partial | Good default; use cost ≥12 |
| scrypt | Yes | Partial | Better than bcrypt, less tooling |
| Argon2id | Yes | Yes | Preferred for new projects |
For new projects: use Argon2id. It won the Password Hashing Competition (2015), is memory-hard, resists GPU and ASIC attacks, and is now in OWASP’s top recommendation. The API is nearly identical to bcrypt.
For existing projects: if you’re already on bcrypt with a sane cost factor, migrating isn’t urgent. Add it to your next major refactor.
Common Implementation Mistakes
Hashing the hash. Some developers hash the password client-side before sending, then hash it again server-side. The client-side hash becomes the “password.” You’re now hashing a fixed-length hex string, not a human-chosen password — you lose nothing by the double hash, but you gain nothing either, and you introduce confusion.
The 72-byte truncation problem. bcrypt silently ignores everything beyond 72 bytes. A 100-character password and a 72-character password that share the same first 72 bytes are identical to bcrypt. If your users set very long passwords, this is a silent security degradation. Mitigation: pre-hash with SHA-256 before passing to bcrypt — but only if you fully understand the implications, and document it clearly.
Weak work factor. Cost 10 was reasonable in 2011. In 2026, use at least 12. If your existing records use cost 10, you can upgrade transparently: after a successful login, re-hash the verified password with the new cost and store the updated hash.
Async matters. bcrypt is CPU-intensive. Always use the async API (as shown above) in Node.js to avoid blocking the event loop. Synchronous bcrypt in a Node server will make every other request wait.
Migrating from MD5/SHA to bcrypt
You can’t re-hash without the original plaintext. But you can migrate opportunistically:
- Add a
password_hashcolumn alongside the oldpassword_md5column - On successful login (when you have the plaintext), bcrypt-hash it and store in
password_hash, clear the old column - After a migration period, users who haven’t logged in can be forced to reset their password
- Once
password_md5is empty for all users, drop the column
This is the standard approach and requires no downtime.
The Bottom Line
Encryption is for data you need to retrieve. Hashing is for data you need to verify. Passwords should be verified, not retrieved — which means encryption is the wrong tool.
bcrypt gives you salting, configurable slowness, and a self-contained hash format. It’s been the right answer for 25 years. Use it at cost 12 or higher, use Argon2id for new projects, and migrate off MD5 and SHA-256 as soon as you can.
Getting this wrong is not a hypothetical risk. Databases get breached. When they do, properly bcrypt-hashed passwords are computationally impractical to crack. MD5 hashes are cracked overnight.
Install Our Extensions
Add IO tools to your favorite browser for instant access and faster searching
恵 Scoreboard Has Arrived!
Scoreboard is a fun way to keep track of your games, all data is stored in your browser. More features are coming soon!
Must-Try Tools
View All New Arrivals
View AllUpdate: Our latest tool was added on Apr 26, 2026
