Password Hashing 101: Bcrypt vs Argon2 and Why Bcrypt Is Still the Default
The first time I joined a company that had a leaked database in its past, the engineering lead handed me a laptop and a single instruction: "figure out which user passwords are still recoverable." They had been hashed with unsalted MD5. I was able to crack a third of them on a single GPU before lunch. Nothing teaches you to take password hashing seriously like watching your own users' secrets fall out of a rainbow table.
Hashing Is Not Encryption (and That Matters)
Encryption is reversible. If you encrypt a password with AES-256-GCM, anyone who later gets the key can read every password back as plaintext. That is exactly the property you do not want for stored credentials. A password database should be a one-way street: a legitimate user types something, you transform it the same way you did at signup, and you compare the two transformations. The original value never has to come back.
That one-way transformation is a hash. Cryptographic hash functions take an input of any length and produce a fixed-length digest, deterministically and (when designed well) without any practical way to invert. The catch: a generic hash function like SHA-256 is too fast for password storage. Modern GPUs can compute billions of SHA-256 hashes per second, which means an attacker who steals your database can guess billions of passwords per second too.
Password hashing functions are a different species. They are designed to be slow on purpose, with parameters you can tune as hardware gets faster. The three names you will run into in production are bcrypt, scrypt, and Argon2. PBKDF2 is the older sibling everyone still has to support for compliance reasons. Each takes a different approach to the same problem: how do you make guessing expensive without making login painful?
Why SHA-256 Alone Is Not Enough
I have seen developers reach for SHA-256 for password storage because it is "the secure one." SHA-256 is excellent for many things: file integrity, signatures, content addressing. It is also the wrong tool here. The exact properties that make SHA-256 great elsewhere (speed, no state, simple parallelism) are what break it for passwords.
Three weaknesses show up the moment a database leaks:
- Speed. A consumer GPU can compute roughly
10^10SHA-256 hashes per second. Most user passwords have less entropy than that. - Determinism without salt. The same password always hashes to the same digest. Attackers precompute massive lookup tables (rainbow tables) once and reuse them against every leaked database forever.
- No memory cost. SHA-256 needs a few hundred bytes of state. Modern GPUs and ASICs run thousands of instances in parallel because nothing competes for memory.
Rule of thumb: if your hash function does not have a tunable cost parameter (rounds, iterations, time, memory), it is not a password hash. SHA-256, MD5, SHA-1, BLAKE2, and SHA-3 are all general-purpose primitives. Use them inside a KDF, not directly.
Salt and Pepper
A salt is a unique random value mixed into the hash for every password. Two users with the same password get two different stored values, and rainbow tables stop being useful because an attacker would have to rebuild one per salt. Salts do not need to be secret; they just need to be unique and unpredictable. Sixteen random bytes from a cryptographically secure RNG is the standard.
Bcrypt, scrypt, and Argon2 all generate salts automatically and store them inside the encoded output, which means you do not have to manage them yourself. A bcrypt hash like $2b$12$N9qo8uLOickgx2ZMRZoMye... already contains the algorithm version, the cost, the salt, and the digest, all in one string. Just save the whole string to your database and you are done.
What About Pepper?
A pepper is an additional secret value, the same for every user, that is not stored in the database. It usually lives in a secrets manager or HSM. The idea is that even if the database leaks, the attacker still has to obtain the pepper from a separate system to start cracking. Pepper is genuinely useful, but only as a layer on top of a real password hash, never as a substitute for one. The most common implementation is to feed each password through HMAC-SHA256 with the pepper as the key before passing the result to bcrypt or Argon2.
Cost Factors: The Knob You Are Allowed to Turn
Every modern password hash exposes one or more parameters that control how expensive it is to compute. This is the entire point. You pick a cost that takes your server about 200 to 500 milliseconds to verify per login, then revisit the value every couple of years as hardware improves. A user does not notice 300 ms during sign-in. An attacker doing offline guessing absolutely does.
- Bcrypt: a single integer called the cost factor or work factor. Each unit doubles the work. Cost
12is the modern baseline; 13 or 14 is reasonable on faster hardware. - Argon2id: three parameters —
m(memory in KiB),t(iterations), andp(parallelism). OWASP currently suggests m=19 MiB, t=2, p=1 as a starting point. - Scrypt:
N,r,p— CPU/memory cost, block size, parallelism. N is typically a power of two, e.g.2^15. - PBKDF2: just an iteration count. Slow, no memory hardness, but FIPS-140 certified. Use 600,000+ iterations for SHA-256 if you are forced into it.
The benchmark you actually care about is wall-clock latency on your production hardware. Spin up a script on a typical instance, vary the parameters, and pick the largest values that keep verification under your latency budget for login.
Bcrypt in Depth
Bcrypt was published in 1999 by Niels Provos and David Mazières. It is built around the Blowfish key schedule, deliberately the slowest part of Blowfish, and exposes a single cost factor that doubles the time per increment. Twenty-five years later it is still the most commonly deployed password hash on the internet. There is something to be said for boring infrastructure that keeps working.
The Encoded Format
$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│ │ │ │
│ │ │ └── 31자 base64 다이제스트
│ │ └── 22자 base64 솔트
│ └── cost factor (2^12 = 4096 키 셋업 라운드)
└── 알고리즘 버전 ($2a, $2b, $2y 등)That single string is everything you need to verify a password later: algorithm, cost, salt, and digest. Store it in a column of length VARCHAR(60)and do not parse it manually. Your library's verify() function reads the prefix, picks the right routine, and runs constant-time comparison for you.
The 72-Byte Limit
Bcrypt silently truncates passwords to 72 bytes. If a user enters a 200 character passphrase, only the first 72 bytes count, and the remaining characters are thrown away. For most humans this never matters, but if you accept passwords from password managers or paste-buffer flows, it can produce surprising results. The standard workaround is to pre-hash with HMAC-SHA256 (or plain SHA-256) and base64-encode the digest before passing it to bcrypt, which gives you a uniform 64-byte input regardless of how long the original was.
When Bcrypt Wins
- Mature, audited libraries in essentially every language ecosystem.
- One parameter to tune. It is hard to misconfigure on accident.
- Self-contained encoded output: no separate salt column.
- Roughly 25 years of public scrutiny without a credible practical break.
Argon2 (Argon2id) in Depth
Argon2 won the Password Hashing Competition in 2015 and is the algorithm OWASP recommends for new applications. The design intentionally fights GPUs and ASICs by being memory-hard: each hash needs to read and write a large block of RAM in a pattern the attacker cannot easily parallelize. A GPU has plenty of compute but comparatively little memory bandwidth per core, so memory-hardness levels the playing field.
Three Variants
- Argon2d— data-dependent memory access. Maximum resistance to GPU/ASIC attacks but vulnerable to side-channel timing attacks if your server runs untrusted code on the same hardware.
- Argon2i— data-independent memory access. Side-channel resistant but slightly weaker against tradeoff attacks.
- Argon2id— hybrid, runs Argon2i for the first half and Argon2d for the second. This is the variant you should pick.
The Encoded Format
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
│ │ │ │ │
│ │ │ │ └── base64 다이제스트
│ │ │ └── base64 솔트
│ │ └── m=메모리(KiB), t=반복, p=병렬도
│ └── 알고리즘 버전 (0x13 = 19)
└── 변형 ($argon2i, $argon2d, $argon2id)Pick Memory Before Time
The trick with Argon2id is to push m as high as your server can spare without choking concurrent logins, then add t until you hit your latency budget. Memory cost is what hurts attackers. Increasing iterations alone slows down your honest users about as much as it slows down a GPU farm.
OWASP 2024 baseline for Argon2id: m = 19 MiB, t = 2, p = 1, with a 16-byte salt. If your service handles sustained login load, you can reduce memory to 12 MiB and bump iterations to compensate, but verify the latency on real hardware before committing.
Scrypt and PBKDF2: Worth Knowing About
Scrypt (Colin Percival, 2009) was the first widely deployed memory-hard password hash and is still a perfectly reasonable choice. The parameter trio (N, r, p) is a little harder to reason about than Argon2's, and it has had less analysis than Argon2 since the latter became the PHC winner, but well-tuned scrypt is closer to Argon2id than to bcrypt in terms of attacker resistance.
PBKDF2 (RFC 2898) is the elder statesman. It is just a deliberately repeated HMAC, with no memory hardness at all. The reason it still shows up is FIPS-140 compliance: U.S. federal procurement and some financial regulators require FIPS-validated cryptographic modules, and PBKDF2 is on the approved list while Argon2 and bcrypt are not. If you have to use it, pick HMAC-SHA256 with at least 600,000 iterations as of 2026, and plan to revisit the iteration count yearly.
Bcrypt vs Argon2 at a Glance
The honest answer to "which one should I use?" depends on what you are optimizing for. Here is how they line up on the dimensions that actually matter in production:
| Property | Bcrypt | Argon2id |
|---|---|---|
| First published | 1999 | 2015 (PHC winner) |
| Memory hardness | No (~4 KiB) | Yes (configurable) |
| Tunable params | 1 (cost) | 3 (m, t, p) |
| Input length limit | 72 bytes | Effectively unbounded |
| GPU resistance | Moderate | Strong |
| FIPS-140 status | Not approved | Not approved |
| Library maturity | Excellent everywhere | Good and growing |
| Misconfiguration risk | Low (one knob) | Moderate (three knobs) |
For a brand new project on a modern stack, Argon2id is the better technical choice. For a project that needs to ship today, with hashing happening across half a dozen language runtimes that each have to interop, bcrypt is the path of least surprise. Both are dramatic improvements over "SHA-256 with a salt." The cost of getting it wrong is a CVE; the cost of picking the slightly less optimal of these two is essentially zero.
Migrating Existing Hashes Without Breaking Logins
Most teams that ask about password hashing already have a database full of weaker hashes. The good news: you do not have to force a password reset on everyone. You upgrade hashes opportunistically, on the next successful login.
The pattern looks like this. Add a column for the new hash. On each login, verify the password against whichever hash exists for that user. If the verification succeeds and the hash is still in the old format (or is bcrypt with a stale cost factor), rehash the plaintext you just verified with the new algorithm or new parameters and overwrite the column.
// 의사 코드 — 로그인 시 점진적 마이그레이션
async function login(email, password) {
const user = await db.users.findByEmail(email);
if (!user) return Errors.INVALID_CREDENTIALS;
// 1. 어떤 알고리즘이든 검증 (인코딩 prefix로 분기)
const ok = await passwordHasher.verify(user.passwordHash, password);
if (!ok) return Errors.INVALID_CREDENTIALS;
// 2. 해시가 오래됐거나 cost가 낮으면 재해싱
if (passwordHasher.needsRehash(user.passwordHash, currentParams)) {
const upgraded = await passwordHasher.hash(password, currentParams);
await db.users.update(user.id, { passwordHash: upgraded });
}
return issueSession(user);
}Most libraries ship a needsRehash helper exactly for this. PHP has password_needs_rehash, node-argon2 has needsRehash, and Spring Security exposes one through DelegatingPasswordEncoder. Use them.
Timing Attacks and Constant-Time Compare
When you compare two hash digests to decide whether a login is valid, do not use == or any comparison that returns as soon as the first mismatched byte is found. A clever attacker can measure response times across many requests and learn which prefix is correct one byte at a time. Every reputable password hash library does the comparison in constant time internally, but if you ever find yourself rolling your own check, reach for crypto.timingSafeEqual in Node, hmac.compare_digest in Python, or subtle.ConstantTimeCompare in Go.
The other timing leak that catches teams off guard: the "user not found" branch of login. If you skip the hash verification when the email does not exist, an attacker can enumerate which emails are registered just by watching response latency. The fix is to always run the hash verification, even against a dummy hash, when the user record is missing. Login should take roughly the same amount of time whether the email exists or not.
Pitfalls I See in Code Reviews
Truncating or transforming the password before hashing
Lowercasing "to be consistent," stripping whitespace, replacing curly quotes with straight ones — these are all destroying entropy you cannot get back. The only safe normalization is Unicode NFC, applied identically at signup and at login. If you do not understand why that step is needed, do not add it.
Logging passwords or hashes
Audit your structured logs. The hash itself is not as bad as plaintext, but it is still a secret: an attacker who scrapes log files can crack hashes offline. Make sure your request logger does not include the request body for auth endpoints, and that no exception handler prints the user object verbatim.
Reusing the same hash for different purposes
Some teams use the password hash as the session token, the API key, or the entropy source for an HMAC. Do not. Hashes for credential storage live in a tightly controlled column with no read access except by the auth service. The moment they show up in URLs, JWTs, or cookies, the threat model collapses.
Never: implement password hashing yourself. The history of cryptography is a history of clever ideas that were quietly broken three years later. Use a library that has been audited, has CVE coverage, and ships sensible defaults. Bcrypt and Argon2 libraries exist for every language you might be using.
Try It with BeautiCode Tools
Reading about password hashing only gets you so far. The fastest way to internalize how cost factors and salts behave is to generate a few hashes and watch how they change. All of these tools run entirely in your browser; no input is sent to a server.
- Bcrypt Hash Generator — hash the same password twice and confirm the digests differ because the salt is fresh each time. Bump the cost from 10 to 14 and notice how the latency climbs.
- Password Generator — produce a high-entropy passphrase or a random 24-character string to feed into the hasher. Use it to seed test fixtures rather than reusing "password123."
- Hash Generator — compute SHA-256 of a known input and time how long it takes. Compare it mentally with the bcrypt run above to feel the difference between a general-purpose hash and a password-grade one.
- HMAC Generator — experiment with the "pepper" pattern by HMAC-ing a password with a server-side key before passing the result through bcrypt.
Frequently Asked Questions
Do I need to upgrade existing bcrypt hashes to Argon2id?
If your bcrypt hashes use a sensible cost factor (12 or higher), you do not need to migrate urgently. Bcrypt is still the most widely deployed password hash on the internet, and it has held up well. The argument for Argon2id is incremental: better resistance to GPU/ASIC-accelerated attacks because of memory-hardness. If you decide to switch, do it opportunistically on login rather than forcing every user to reset.
How often should I revisit the cost factor?
Aim to recheck once a year and any time you upgrade your application servers. As hardware gets faster, the same cost factor is doing less work relative to attackers. A common practice is to bump cost by one when verification time drops below ~150 ms on a typical request handler. Combine that with the rehash-on-login pattern so the upgrade rolls out organically.
Can I store the salt in a separate column?
You can, but you almost never should. Bcrypt and Argon2 both encode the salt into the output string by design, which means you cannot accidentally lose the link between a salt and its hash. If you split them, the next person on the team will eventually swap rows, truncate columns, or build a backup that puts the two out of sync. Keep them together.
What if my user types a 100-character passphrase into bcrypt?
Bcrypt will only consider the first 72 bytes. The remaining characters are ignored, which is surprising to users who deliberately picked a long passphrase. The standard mitigation is to pre-hash the password with HMAC-SHA256 (or plain SHA-256) and base64-encode the digest before handing it to bcrypt. This produces a fixed 64-byte string regardless of input length and removes the silent truncation risk. Argon2 does not have this limitation.
Should I tell the user how strong their password is at signup?
Yes, but use a real strength estimator like zxcvbnrather than "must contain a number and a symbol." Composition rules pushed people toward Password1!and that is exactly the family of guesses cracking dictionaries hit first. Pair length-based feedback with rejection of breached passwords (HIBP's k-anonymity API works well) and you will block far more real attacks than any rule set will.
Related Tools
Bcrypt Hash
Generate and verify bcrypt password hashes with configurable rounds.
Password Generator
Create strong, secure passwords with customizable options and strength indicators.
Hash Generator
Generate MD5, SHA-1, SHA-256, SHA-384, SHA-512 hashes from text with real-time computation.
HMAC Generator
Generate HMAC signatures using SHA-256, SHA-384, SHA-512 with a secret key.
Related Articles
How to Generate Secure Passwords in 2026: A Complete Guide
Learn why strong passwords matter and how to generate secure passwords using entropy, length, and complexity. Includes practical tips and free tools.
2025-12-15 · 8 min readData FormatsJSON vs YAML: When to Use What — A Developer's Guide
Compare JSON and YAML formats with syntax examples, pros and cons, and use case recommendations for APIs, configs, and CI/CD pipelines.
2025-12-28 · 10 min read