Password hashing: bcrypt, scrypt, and Argon2
Why you should never store passwords in plain text or a fast hash, and how bcrypt, scrypt, and Argon2id protect them with salt, work factor, and real memory cost.
Database breaches happen all the time. The question isn't whether your users table will one day end up on a forum, but what the attacker finds when it does. If you stored passwords in plain text, the game is over the instant of the dump. If you stored them with MD5 or SHA-256, the game is over a few hours later, because those algorithms were designed to be fast — and speed is exactly the property you do not want when protecting a password. This article explains why storing passwords demands a deliberately slow hash, what separates a decent password hash from a generic one, and how bcrypt, scrypt, and Argon2 solve the problem.
Why fast hashing is the enemy
There's a common confusion between "cryptographic hash" and "password hash." MD5, SHA-1, SHA-256 are general-purpose hash functions: useful for file checksums, signatures, integrity. Their goal is to process gigabytes per second. That's the problem. A modern GPU computes billions of SHA-256 hashes per second. If your database leaked and the passwords were in SHA-256, the attacker simply runs a dictionary of common passwords plus variations and matches each guess against the hash. An eight-character lowercase password falls in minutes.
There's also the rainbow table: precomputed tables that map a hash back to the original password. Against plain SHA-256, with nothing extra, they work, because the same input always produces the same output. Hashing "123456" with SHA-256 always gives the same value, on any server on the planet.
The conclusion is straightforward: a password hash can't be fast, and can't be naively deterministic. It needs three properties that MD5 and SHA don't have.
What a password hash needs
First, it must be slow and tunable. You want computing a single hash to cost, say, 100 to 250 milliseconds. For your login that's imperceptible. For an attacker trying billions of guesses, it becomes a wall. And "tunable" matters because hardware gets faster every year — you need a parameter, the work factor, that lets you raise the cost over time without switching algorithms.
Second, it must have a unique salt per password. The salt is a random value generated for each user and concatenated to the password before hashing. It kills rainbow tables outright: since each password has a different salt, two people with the password "123456" produce completely distinct hashes, and no precomputed table is of any use. The salt isn't secret — it's stored alongside the hash, in plain text.
Third, it should ideally be memory-hard: forcing the computation to consume a significant amount of RAM. This attacks the adversary's economic model directly. GPUs and ASICs have thousands of cheap cores, but fast memory is expensive and limited. An algorithm that requires, say, 64 MB per computation knocks out the massive parallelism that makes a GPU attack viable.
bcrypt: the solid veteran
bcrypt has existed since 1999 and remains perfectly defensible. It's derived from the Blowfish cipher, has a built-in work factor (the "cost," typically between 10 and 14, where each increment doubles the work) and generates the salt automatically. The result is a string that carries everything inside it:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
Reading left to right: $2b$ is the algorithm version, 12 is the work factor, the next 22 characters are the salt, and the rest is the hash. Salt and cost travel with the hash — you store nothing separately.
bcrypt has one quirk worth knowing: it truncates the input at 72 bytes. Passwords (or long passphrases) above that have the excess silently ignored, which in extreme cases becomes a security problem. For most applications, 72 bytes is more than enough, but it's good to know. Calibrating the cost is practical: generating and checking a bcrypt hash with different work factors helps find the value that yields ~200 ms on your server.
scrypt and Argon2: the memory-hard generation
scrypt (2009) was one of the first to treat memory as a defense resource. Beyond CPU cost, it has memory and parallelism parameters, so an attacker can't just throw more cores at the problem — they need more RAM per core, which raises the cost of a GPU/ASIC attack. It's solid and widely available.
Argon2 is the state of the art. It won the Password Hashing Competition (PHC) in 2015, precisely the competition created to choose a worthy successor. It comes in three variants: Argon2d (resistant to time-memory attacks but vulnerable to side-channel), Argon2i (resistant to side-channel), and Argon2id, which combines both approaches. For login passwords, the current and nearly universal recommendation is Argon2id. It exposes three independent parameters: time cost (iterations), memory cost (how much RAM), and parallelism (how many threads). That gives you fine control over the trade-off between security and the load on your server.
How verification works in practice
Because the hash carries the salt and parameters inside it, you don't need to store anything separately or reimplement the logic. At signup, you generate the hash and save the whole string. At login, you pass the typed password and the saved hash to the verify function, which extracts the salt and cost from the hash itself, recomputes, and compares:
// signup
hash = argon2id_hash(password) // salt generated and embedded
save(user, hash)
// login
hash = fetch_hash(user)
if argon2id_verify(typed_password, hash):
// ok
Never compare hashes manually with ==; use the library's verify function, which performs a constant-time comparison to avoid timing attacks.
Two final details. The pepper is an extra secret, the same for the whole application, stored outside the database (in an environment variable or vault) and mixed into the password before hashing. If only the database leaks, the pepper keeps protecting — it's an optional but cheap layer. And rehashing: when you raise the work factor (because hardware got faster, or because you migrated from bcrypt to Argon2id), do it opportunistically on the user's next successful login, when you have the plain-text password in hand for an instant. Detecting that a hash uses old parameters and rewriting it is trivial.
Don't confuse this with reversible encryption
It's worth separating two worlds. A symmetric cipher like AES is reversible by design: you encrypt to later decrypt with the key. That's for data you need to recover — a card number, a message, a file. A password isn't like that. You never need to recover the original password; you only need to confirm that whoever is logging in knows the same password. That's why a hash is one-way and intentionally so: not even you can reverse it. If you want to understand the bigger picture of ciphers and keys, it's worth reading symmetric vs asymmetric cryptography, but keep the rule: passwords get hashed, recoverable data gets encrypted.
Frequently asked questions
Is bcrypt still secure in 2026?
Yes, with the caveat of using it with an adequate work factor (12 or more, ideally calibrated for ~200 ms on your hardware) and being aware of the 72-byte limit. bcrypt isn't memory-hard, so in theory it's cheaper to attack on a GPU than Argon2id, but for the overwhelming majority of applications it remains perfectly acceptable and is everywhere — nearly every language has a mature implementation.
Which should I pick for a new project?
Argon2id. It's the recommendation of the PHC, of OWASP, and of most current guides, because it attacks time cost and memory cost at the same time. Start with conservative parameters (for example 19 MB of memory, 2 iterations, 1 thread) and tune by measuring the real time on your server. If your stack only has bcrypt available and well tested, bcrypt remains a defensible choice.
Do I need to store the salt in a separate column?
No. bcrypt, scrypt, and Argon2 all embed the salt (and the cost parameters) in the output string itself. You store a single text column with the full hash, and the verify function extracts everything it needs from there. Creating a separate salt column is an old, unnecessary pattern with these algorithms.
Should I always use a pepper?
It's not mandatory, but it's cheap and it helps. The pepper specifically protects the scenario where only the database leaks, not the whole application server with its environment variables. The golden rule: the salt stays with the hash, the pepper stays outside the database. Never invent your own scheme to "scramble" the password beyond that — you'll only introduce weakness.
What to take away
Passwords never in plain text, never in a fast hash. Use Argon2id for a new project; accept bcrypt where it's already well deployed. Trust the embedded salt, consider a pepper outside the database, and rehash when you raise the cost. And above all, don't invent your own hashing scheme — the mature libraries have already solved the hard details, and your job is just to pick good parameters and measure.
- 01 UTF-8 and encoding problems: why accents break and how to fix them What UTF-8 is, code points, mojibake, and why accented text turns to garbage — the mechanism explained end to end.
- 02 MIT, Apache, and GPL Licenses: Practical Differences for Developers MIT, Apache 2.0, and GPL are not the same thing. Understand permissive vs copyleft, patent protection, and compatibility before shipping code to production.