Argon2id and the shape of modern password security
2026-05-19 • 6 min read | Why slow, memory-hard password hashing matters, and why Argon2id is the default I trust for modern password-style credentials.
tags: security authentication argon2 password-hashing cryptography web-security Table of Contents
Open Table of Contents
Argon2id and the shape of modern password security
Passwords are still the most common way we protect digital identity.
Which is slightly unfortunate, because passwords are also very easy to store badly.
Plaintext storage is the obvious disaster case. If the database leaks, every account is immediately exposed. No cracking. No cleverness. Just a table full of reusable credentials.
But the less obvious failure is storing passwords with a hash that was never meant for password storage.
That mistake looks more respectable.
It is still a mistake.
This is the part where the algorithm choice matters more than it first appears.
$ whatis hashing
A hash function takes some input and produces a deterministic output.
For example:
Password: Pa$$w0rd!
Hash: $2a$12$Knv44RtzDz1wjxaP1BZEH.dK49wX4xnv...The important property is that hashes are one-way. You should not be able to take the hash and reverse it back into the original password.
During login, the server does not decrypt the stored value. It hashes the submitted password again and checks whether the result matches.
The problem is that “one-way” is not enough.
If an attacker gets the stored hashes, they can guess passwords, hash those guesses, and compare the outputs. They do not need your login form anymore. They can do the work offline, as fast as their hardware allows.
That is where ordinary hashing stops being enough.
A password hash is not there to make the happy path clever. It is there to make the bad day expensive.
$ cat ~/why-fast-hashes-fail.md
MD5, SHA-1, and SHA-256 are fast.
That is not an insult. It is part of what makes them useful.
Fast hashes are good for:
- file integrity
- checksums
- content addressing
- signatures
- deduplication
For those jobs, speed is the point.
For password storage, speed is the problem.
If a leaked password database uses a fast hash, attackers can test enormous numbers of guesses cheaply. GPUs and rented compute are very good at this kind of repetitive work.
So the defensive goal changes.
You do not want password verification to be as fast as possible.
You want it to be expensive enough that large-scale guessing becomes painful, while still being tolerable for legitimate logins.
$ ls ~/password-hashing-history
Password hashing algorithms mostly evolved around one question:
how expensive can we make password guessing without making real users miserable?
The rough lineage looks like this:
| Algorithm | Where it stands | Why it matters |
|---|---|---|
MD5 | obsolete | much too fast |
SHA-1 | obsolete | broken collision resistance, also too fast |
SHA-256 | strong general hash | not designed to slow password guessing |
PBKDF2 | still used | tunable iterations, but CPU-focused |
bcrypt | still common | password-focused, but older constraints |
scrypt | strong | memory-hard and GPU-resistant |
Argon2id | current password security standard | memory-hard, tunable, designed for modern attackers |
Argon2 won the Password Hashing Competition in 2015.
The useful part is not the trophy. The useful part is that Argon2 was designed for the hardware reality password hashes actually face now: GPUs, cheap cloud compute, specialized cracking rigs, and large credential dumps.
$ whatis argon2id
Argon2 comes in three variants:
Argon2dArgon2iArgon2id
Very roughly:
Argon2dleans toward GPU-cracking resistanceArgon2ileans toward side-channel resistanceArgon2idcombines both approaches
For typical web application password storage, Argon2id is the current standard because it has the right shape.
OWASP’s password storage guidance points to Argon2id with a minimum configuration around 19 MiB of memory, 2
iterations, and 1 degree of parallelism.1
$ cat ~/why-memory-hardness-matters.md
The key property is memory hardness.
A memory-hard password hash does not only ask for CPU time. It also asks for RAM.
That matters because attackers do not usually crack one password at a time. They try to run huge numbers of guesses in parallel.
GPUs are excellent at parallel computation.
They are much less happy when every guess also needs a meaningful chunk of memory.
await hash(password, {
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});Different libraries name the knobs differently, but the idea is the same:
- memory cost controls how much RAM each guess needs
- time cost controls how many passes of work happen
- parallelism controls how the work is split
The aim is to make password guessing meaningfully expensive today, then revisit the cost as hardware changes.
Security work is often less about making attacks impossible and more about changing attacker incentives.
$ cat ~/how-to-store-passwords.md
A modern password storage flow is small:
- Receive the password.
- Hash it with Argon2id.
- Store only the resulting hash string.
- Verify future login attempts against that stored hash.
The things you do not do are just as important:
- do not store plaintext passwords
- do not encrypt passwords and plan to decrypt them later
- do not use
MD5,SHA-1, or rawSHA-256 - do not invent your own crypto
- do not manage salts manually unless your library makes you
Argon2 hash strings already include the salt and parameters.
$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>That string is not just the hash output.
It is also a small record of how the credential should be verified.
That matters later. If you raise your parameters, older hashes can still verify, and you can rehash on the next successful login.
The boring metadata is doing real work.
$ cat ~/best-practices.md
If I were implementing password authentication today, the defaults would look something like this:
- use
Argon2idunless the platform gives you a strong reason not to - use a mature, maintained library
- store only the encoded hash string
- let Argon2 handle salts inside that string
- tune the cost on real deployment hardware
- revisit the cost as hardware improves
- protect the database at rest and in transit
- avoid turning raw credentials into session state
That last point is not strictly about Argon2id.
It is still where small auth systems often go wrong.
A good password hash protects the stored verifier. It does not automatically fix cookies, CSRF, session rotation, logging, deployment workflows, or credentials pasted into places they do not belong.
The hash is one boundary.
The rest of the system still has to have boundaries too.
$ cat ~/where-this-showed-up-here.md
This became relevant while I was cleaning up the guestbook admin flow on this site.
The old version used a raw ADMIN_TOKEN. That token lived in environment config, could become the browser cookie value,
and used to have a shortcut path through ?token=... URLs.
None of that required a user table.
It was still password-shaped enough to deserve password-storage thinking.
So the migration moved from this:
ADMIN_TOKEN=<raw reusable secret>to this:
ADMIN_TOKEN_HASH=$argon2id$...The raw token stays with the operator. The deployment gets only the Argon2id verifier. The browser gets a signed session cookie, not the reusable credential.
That distinction is the real point.
Argon2id made the stored secret safer. The rest of the migration made sure the raw secret stopped doing every job in the system.
I wrote the implementation details separately here:
Replacing raw admin tokens with Argon2-backed auth
That post gets into the middleware, cookies, async auth plumbing, query-token removal, and the deployment details.
$ echo $tldr
Passwords are simple strings.
Password storage is not simple.
The important question is not only whether an attacker can crack one password. It is whether your system makes large-scale guessing cheap once hashes leak.
Modern password hashing intentionally wastes:
- CPU time
- memory
- parallel compute efficiency
because attackers have plenty of all three.
That is why Argon2id is the password hash I reach for.
Not because it makes breaches harmless.
Because it changes the economics in the defender’s favour, and it does so with a format that is practical to store, verify, tune, and upgrade.
That is usually what I want from security-sensitive code.
Small enough to understand.
Boring enough to trust.