bcrypt Password Hashing Explained for Developers
Storing user passwords is one of those tasks that looks trivial until you actually think about it. Plaintext is obviously out. MD5 and SHA-256 seem tempting because they are fast, but that is exactly the problem. bcrypt is the boring, battle-tested answer most production systems land on, and it has been for over 20 years. Here is what it actually does, why speed is not what you want, and how to use it without shooting yourself in the foot.
Table of Contents
Introduction
Every developer hits this moment eventually. You are building a login system, you need to store passwords, and the question of "how" is staring you in the face. The bad answers are obvious. Plaintext is career-ending. A simple MD5 is basically the same as plaintext to anyone with a rainbow table and a laptop. Even SHA-256, which sounds official and secure, is the wrong tool for this job.
bcrypt is the tool you actually want. It was designed in 1999 specifically for password storage, and it is still the default recommendation in 2026 for most web applications. It is slow on purpose. It salts automatically. And it has a dial that lets you keep it slow as hardware gets faster.
If you just want to generate a bcrypt hash to test something, StackConvert's hash generator handles it right in your browser. The rest of this article is about why bcrypt exists and how to use it properly in real code.
What is bcrypt
bcrypt is a password hashing function. Given a password, it produces a fixed-length string that represents the password without being the password. The original cannot be recovered from the hash. When a user tries to log in, you hash their attempt with the same parameters and compare it against the stored hash.
What makes bcrypt different from general-purpose hashes like SHA-256 is that it is designed to be slow and expensive to compute. That sounds like a bug, but it is actually the whole point. If hashing takes 250 milliseconds, your login endpoint barely notices the delay. An attacker trying to brute force billions of passwords, on the other hand, now has a very bad day.
Why Not Just Use SHA-256
SHA-256 is great at what it was built for: hashing files for integrity, signing transactions, verifying data. It is extremely fast. That is a feature for those use cases. For passwords, it is the opposite of a feature.
| Algorithm | Hashes per Second (Modern GPU) | Time to Crack 8-char Password |
|---|---|---|
| MD5 | Over 100 billion | Seconds |
| SHA-256 | Tens of billions | Minutes to hours |
| bcrypt (cost 12) | A few hundred | Years to centuries |
That last row is the whole reason bcrypt exists. An attacker who steals your password database can attempt billions of SHA-256 guesses per second per GPU. Against bcrypt at a sensible cost factor, they are stuck at a few hundred guesses per second per GPU. Same hardware, massively different timeline.
The other issue with plain SHA-256 is that it is deterministic and unsalted. If two users pick the same password, they get the same hash. Attackers precompute giant tables of common password hashes (rainbow tables) and just look up yours. bcrypt bakes a unique random salt into every hash, which makes rainbow tables useless.
How bcrypt Works
Under the hood, bcrypt does something that sounds almost silly. It takes a cipher called Blowfish, intentionally runs a very expensive key setup on it, and uses that setup step as the hashing operation. The whole thing is designed to be hostile to hardware acceleration. You cannot just throw a GPU at it and go 1000x faster, the way you can with SHA-256.
The process at a high level:
- Generate a random 16-byte salt.
- Run bcrypt's expensive key setup using the password and the salt. How expensive depends on the cost factor.
- Encrypt a fixed string with the resulting key, repeatedly.
- Encode the salt, cost factor, and result into a single 60-character string. That is the final hash.
The important detail is that the salt and the cost factor are stored right inside the hash string itself. You do not need a separate column in your database for either one. When verifying a password, bcrypt reads the salt and cost out of the stored hash and runs the exact same process.
The Cost Factor Explained
The cost factor, sometimes called work factor or rounds, is bcrypt's superpower. It is an integer that controls how expensive the hashing is. Each time you increase it by 1, the hashing time roughly doubles.
| Cost Factor | Approx. Time (Modern Server) | Recommendation |
|---|---|---|
| 8 | ~15ms | Too fast now |
| 10 | ~60ms | Old default, weak today |
| 12 | ~250ms | Good modern default |
| 14 | ~1s | Sensitive apps, higher security |
| 16 | ~4s | Getting noticeable on login |
The sweet spot for most web apps in 2026 is cost 12. It takes roughly a quarter of a second to verify a password, which users will not feel, and it makes brute force attacks impractical on current hardware. When hardware gets twice as fast, bump it to 13. That is the point of the cost factor being adjustable: bcrypt ages gracefully.
One important thing: the cost factor is stored inside the hash. So you can raise the cost for new users or when existing users log in, without invalidating old hashes. More on that pattern below.
Anatomy of a bcrypt Hash
A bcrypt hash is a 60-character string that looks something like this:
$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWyIt is not random garbage. It is a structured, self-describing format:
- $2b$ - The algorithm version. $2b is the current standard. Older hashes may start with $2a or $2y, which are essentially compatible variants.
- 12$ - The cost factor. Here it is 12.
- N9qo8uLOickgx2ZMRZoMye - The first 22 characters after the cost are the base64-encoded salt (16 bytes).
- IjZAgcfl7p92ldGxad68LJZdL17lhWy - The remaining 31 characters are the base64-encoded hash output (24 bytes).
Because everything bcrypt needs is inside that string, you only need one column in your database: a CHAR(60) or VARCHAR(60). No separate salt column, no separate cost column.
Code Examples
Node.js (bcryptjs or bcrypt)
const bcrypt = require('bcrypt')
// Creating a hash when a user signs up
const hash = await bcrypt.hash(plainPassword, 12)
// Store hash in your database
// Verifying a password at login
const isValid = await bcrypt.compare(plainPassword, storedHash)
if (isValid) {
// Log the user in
}Python (bcrypt library)
import bcrypt
# Create a hash
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))
# Verify a password
if bcrypt.checkpw(password.encode('utf-8'), hashed):
# Log the user in
passGo (golang.org/x/crypto/bcrypt)
import "golang.org/x/crypto/bcrypt"
// Create a hash
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
// Verify a password
err := bcrypt.CompareHashAndPassword(storedHash, []byte(password))
if err == nil {
// Password matches
}PHP (password_hash)
// Create a hash (PASSWORD_BCRYPT uses bcrypt)
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Verify
if (password_verify($password, $hash)) {
// Password matches
}The pattern is the same in every language: one function to hash, one function to verify. You do not need to manage salts or cost factors manually. The library handles all of it as long as you pass a reasonable cost.
Common Mistakes to Avoid
- Rolling your own by combining SHA-256 with a salt. This feels clever but misses the point. Speed is the vulnerability, not missing salt. Use a function designed for this.
- Using cost 10 because it is the old default. Cost 10 was fine in 2012. It is weak now. Use at least 12 in 2026, and review every couple of years.
- Comparing hashes with a plain string equality check. Use the library's compare or verify function, which is constant-time and resistant to timing attacks.
- Forgetting that bcrypt silently truncates passwords at 72 bytes. If your users can pick passwords longer than that, consider pre-hashing with SHA-256 first, or using Argon2 instead.
- Hashing on the client side. Never do this. The client sends whatever hash the browser computed, and that hash effectively becomes the password. Always hash on the server.
- Not upgrading old hashes. When a user logs in, check the cost of the stored hash. If it is lower than your current standard, re-hash the password during that same login while you still have the plaintext in memory.
When to Use Something Other Than bcrypt
bcrypt is a fine default for most web apps. There are a few situations where you might want an alternative:
- Argon2. Won the Password Hashing Competition in 2015 and is technically more modern. It is memory-hard in addition to being slow, which makes GPU attacks even harder. If you are starting fresh today and your language has a mature Argon2 library, it is a reasonable choice. But bcrypt is still perfectly fine and more widely supported.
- scrypt. Also memory-hard. Widely used in cryptocurrency wallets. Fine for passwords, but Argon2 has largely replaced it in new projects.
- PBKDF2. FIPS-certified and required in some regulated environments (US federal work, some financial systems). Use it when compliance forces you to. Otherwise, bcrypt or Argon2 is a better choice.
Do not use MD5, SHA-1, SHA-256, or SHA-512 directly for passwords, ever. They are the right hash functions for other things. Password storage is not one of them.
Common Questions
Can I store bcrypt hashes in a regular database column?
Yes. A CHAR(60) or VARCHAR(60) column is all you need. Everything, including the salt and cost factor, is encoded inside the hash string.
Do I need to store salts separately?
No. bcrypt handles the salt for you and stores it inside the hash. Every password you hash gets a fresh random salt automatically.
What happens if two users pick the same password?
They get completely different hashes, because each one is generated with a unique random salt. This is the defense against rainbow tables.
Should I use bcrypt or bcryptjs in Node.js?
Either works. The native bcrypt package is faster because it wraps a C implementation. bcryptjs is slower but pure JavaScript, so it runs anywhere including serverless cold starts without native build steps. For high-volume auth, prefer bcrypt. For portability and simpler deploys, bcryptjs is fine.
How often should I upgrade my cost factor?
Review every 2 to 3 years. When hardware gets twice as fast, bump the cost by 1 to compensate. You can rehash existing users transparently on login, as described above.
Is bcrypt broken or outdated?
No. It is over 20 years old and has aged remarkably well. The only real critique is that it is not memory-hard, which is why Argon2 exists. But no practical attack has broken bcrypt when used with a modern cost factor, and it remains a fully acceptable choice in 2026.
Wrapping Up
Password storage is one of those areas where "boring" is the right answer. bcrypt is boring. It has been around forever. Every major language has a solid library for it. The correct usage pattern is one function to hash and one function to verify, and that is mostly all you need to know. Pick cost 12, hash on the server, never roll your own, and upgrade when hardware catches up.
If you want to see what a bcrypt hash actually looks like, or you need a quick one for testing, the hash generator will produce one in your browser. No backend call, no signup, and you can play with different cost factors to see how the output changes.