Choosing the Right bcrypt Cost Factor in 2026 (OWASP Guide)
The bcrypt cost factor is the single most important number in your password storage configuration. Too low and a leaked database becomes a quick win for attackers. Too high and your login page times out. The right value depends on your hardware, your traffic, and the year — yes, the year. The recommended minimum has been creeping upward as CPUs get faster.
The TL;DR for 2026
- Minimum acceptable for production: cost 10 (was 8 in 2012, 10 in 2018, still 10 today as a hard floor)
- OWASP recommended baseline: cost 12
- High-security applications: cost 14+
- Target hash time: 200-500ms per hash on your production hardware
- Every increment doubles the work for both you and the attacker
How the cost factor works
bcrypt's cost factor is an exponent. The actual number of internal iterations is 2^cost:
| Cost | Iterations | Approx. time on modern CPU |
|---|---|---|
| 4 | 16 | ~1 ms (debugging only) |
| 8 | 256 | ~30 ms |
| 10 | 1,024 | ~100 ms |
| 11 | 2,048 | ~200 ms |
| 12 | 4,096 | ~400 ms (OWASP baseline) |
| 13 | 8,192 | ~800 ms |
| 14 | 16,384 | ~1.5 s |
| 15 | 32,768 | ~3 s |
| 31 | 2³¹ | ~7 years (don't) |
The "approx time" column is wildly hardware-dependent. A 2024 high-end server CPU is roughly 2-3× faster than what's shown here. A 2010 server is 3-5× slower. Always benchmark on your actual production hardware before settling on a value.
The OWASP cheat sheet says...
OWASP's Password Storage Cheat Sheet (2025 update, still current in 2026) recommends:
For legacy systems using bcrypt, use a work factor of 10 or more, with a password limit of 72 bytes. The work factor should be as large as verification server performance will allow.
OWASP also notes that Argon2id is the first-choice algorithm for new systems, and bcrypt is a fallback for environments without Argon2 support. If you're choosing bcrypt today, that's fine — but understand it's the conservative choice, not the leading-edge one.
How to choose YOUR cost factor
The cost factor should make a single password hash take about 200-500 milliseconds on your production login server. Why this range?
- Faster than 200ms: too easy for attackers to brute-force.
- Slower than 500ms: users notice the lag, and worse, your login endpoint becomes a DoS vector — an attacker who fires 1,000 login attempts at it ties up 500 seconds of CPU time.
- 200-500ms is the sweet spot: imperceptible to humans, expensive enough that brute force is unprofitable.
Run a benchmark on your production server (or one with the same CPU profile):
// Node.js
const bcrypt = require('bcryptjs');
const start = Date.now();
bcrypt.hashSync('test_password_x', 12);
console.log('cost 12:', Date.now() - start, 'ms');
# Python
import bcrypt, time
start = time.time()
bcrypt.hashpw(b'test_password_x', bcrypt.gensalt(rounds=12))
print('cost 12:', round((time.time() - start) * 1000), 'ms')
If cost 12 takes less than 200ms, try 13. If more than 500ms, drop to 11. Pick the highest cost that stays under 500ms.
Concurrency and capacity planning
Login latency isn't the only concern. Each login burns CPU for the duration of the hash. If your average is 300ms per hash and you handle 100 logins/sec at peak, you need 30 CPU-seconds per second — that's 30 dedicated cores doing nothing but hashing passwords.
Mitigations:
- Rate-limit login endpoints aggressively. 5-10 attempts per IP per minute is generous.
- Use account lockouts after N failed attempts.
- Cache successful auth tokens so users aren't re-hashed on every API call.
- Consider Argon2id with lower memory cost if memory is cheaper than CPU in your environment.
Upgrading existing hashes
If your existing database has bcrypt hashes at cost 10 and you want to migrate to cost 12, you don't need a flag day. The standard pattern is "rehash on successful login":
async function login(username, password) {
const user = await db.findUser(username);
if (!user) return false;
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return false;
// Check if hash needs upgrading
const currentCost = parseInt(user.passwordHash.substring(4, 6));
if (currentCost < 12) {
const newHash = await bcrypt.hash(password, 12);
await db.updateUserHash(user.id, newHash);
}
return user;
}
Within a few weeks of normal usage, the majority of active accounts will be upgraded. Inactive accounts can be flagged for forced password reset on next login.
The cost factor over time
Historical bcrypt cost recommendations:
- 1999: cost 6-8 (when bcrypt was invented)
- 2010: cost 10 became the standard
- 2020: cost 11 emerging as a recommendation
- 2024: OWASP shifted explicit recommendation to 12
- 2026: cost 12 baseline, 13-14 for security-sensitive applications
Every year or two, review whether your cost factor still produces 200-500ms hashes on current hardware. If hashing is now under 100ms, increment the cost and start migrating.
The 72-byte gotcha
bcrypt silently truncates passwords at 72 bytes (note: bytes, not characters — a UTF-8 emoji can be 4 bytes). This affects passphrases more than it affects normal passwords, but it's still a footgun.
Standard workaround: pre-hash with SHA-256 before bcrypting:
const sha256 = crypto.createHash('sha256').update(password).digest('base64');
const hash = await bcrypt.hash(sha256, 12);
But OWASP warns this introduces its own subtle issues — null bytes in the SHA-256 output, "password shucking" attacks on systems that store both hashes. If you have passphrases >72 bytes to handle, consider Argon2id, which has no length limit.
Want to see how cost factor affects timing on your own hardware? Use our bcrypt generator — the slider lets you experiment with cost 4 through 15 and shows the actual hashing time for each. A great way to calibrate your production setting.