Bcrypt vs Argon2 covers why Argon2 won the Password Hashing Competition and why its memory-hardness beats bcrypt's fixed 4 KB footprint. What it doesn't cover is what actually happens inside a call to argon2id(password, salt, ...) — three variants that resist different attacks, a memory array filled through a specific, deliberately ASIC-unfriendly construction, and a parameter list with two entries almost nobody uses. This is that layer, straight from RFC 9106.
Three variants, three threat models
Argon2 isn't one algorithm — it's three, distinguished by exactly one design choice: how it decides which earlier block of memory to mix into the block it's currently computing.
- Argon2d picks that reference block using data that depends on the password itself — the index is derived from the content of the previous block, which was itself shaped by the password. That makes the memory access pattern a function of the secret input, which is exactly what maximizes resistance to time-memory trade-off attacks — an attacker trying to save memory by recomputing blocks on the fly instead of storing them gets punished hardest here. The cost: someone watching the pattern of memory addresses touched — a co-tenant on shared hardware doing a cache-timing attack — can learn something about the password. RFC 9106 recommends Argon2d specifically for contexts with no such observer, like cryptocurrency mining and other proof-of-work schemes.
- Argon2i does the opposite: every memory address it will ever touch is determined entirely by public inputs (the pass number, lane, slice, and configured parameters) — never by the password. Run it twice with the same parameters and different passwords, and the memory access pattern is bit-for-bit identical both times. An observer watching cache or memory access timing learns nothing, which is exactly why RFC 9106 calls data-independent addressing "preferred for password hashing." The tradeoff is real, though: this predictability is also what lets a well-resourced attacker mount a cheaper trade-off attack, which is why Argon2i needs meaningfully more passes to reach the same trade-off resistance as Argon2d at the same memory size.
- Argon2id splits the difference by literally switching strategy mid-run: data-independent (Argon2i-style) addressing for the first half of the first pass, then data-dependent (Argon2d-style) addressing for everything after. Early on — while an attacker's trade-off attack has the most leverage — the access pattern reveals nothing. For the rest of the computation, it gets the stronger trade-off resistance of data-dependent addressing. RFC 9106 recommends Argon2id "as a default setting for all environments," and it's the variant OWASP, this site's tools, and nearly every modern default configuration use.
How the memory actually gets filled
Set m KiB of memory and Argon2 doesn't just allocate a flat buffer — it lays it out as a matrix of 1024-byte blocks: p rows, one per lane of parallelism, by q columns, where q works out to roughly m / p. Each lane is further split into 4 vertical slices. Blocks within the same slice, across different lanes, have no dependency on each other and can genuinely be computed in parallel — that's what the p parameter actually buys you on multi-core hardware, not just "more lanes means more secure."
Every block (after the first two per lane, which seed from the password and salt) is computed by a compression function G that mixes the previous block with a reference block chosen by the addressing rule above. G is built on BLAKE2b's internal round function, applied to an 8×8 arrangement of the block treated as rows, then again as columns — but with one deliberate change from stock BLAKE2b: Argon2's version of the round function adds 64-bit multiplications alongside the usual modular additions. RFC 9106 is explicit about why: that extra multiplication step exists specifically to "increase the circuit depth and thus the running time of ASIC implementations," making a hardware implementation genuinely more expensive to build fast, not just memory-hungry.
The t parameter ("iterations") controls how many additional full passes the algorithm makes back over this same memory array after the first one, each pass re-mixing every block again. This is why t is a real, independent cost control even though the memory is already fully written after pass one — it's not padding, it's additional required computation over data that's already there, which is also why raising t doesn't require any more RAM the way raising m does.
The two parameters almost nobody sets
Beyond the memory/time/parallelism trio most guidance focuses on, Argon2's full parameter list has two more entries:
- K, the secret key. An optional value mixed into every block alongside the password, salt, and associated data — kept separate from the salt and never stored with the hash. If that description sounds familiar, it should: Understanding Salt and Pepper covers exactly this concept under the name most people know it by — a pepper. Argon2 formalizes it as a first-class parameter rather than something you bolt on with a separate HMAC step.
- X, associated data. An optional public value — not secret, not random — mixed in for domain separation: binding a hash to a specific application, protocol version, or context so the same password produces a different hash if reused somewhere else. Rarely used in practice for plain password storage, more common in protocols that derive multiple different keys from the same underlying secret.
Two "recommended" parameter sets that don't match — on purpose
RFC 9106 and OWASP's Password Storage Cheat Sheet both hand out Argon2id parameters, and they look nothing alike:
| Source | Memory | Iterations | Parallelism |
|---|---|---|---|
| RFC 9106, first recommended option | 2 GiB | 1 | 4 |
| RFC 9106, second recommended option | 64 MiB | 3 | 4 |
| OWASP baseline | 19 MiB | 2 | 1 |
Neither is wrong — they're answering different questions. The RFC's first option assumes a server whose job is authentication and can dedicate real, multi-gigabyte memory to it per concurrent hash. OWASP's number is a conservative floor meant to be safe on a typical web app server that's also running your application code, your database connections, and everything else, under real concurrent login load. Both documents agree on the actual guidance: treat whichever number you start from as a minimum, and push it higher if your hardware and latency budget allow — Password Hashing Best Practices covers calibrating that tradeoff, and Bcrypt Cost Calculator does the equivalent benchmarking for bcrypt's single cost parameter.
Which variant should you actually use?
- Argon2id, for almost every password storage use case. It's the RFC default recommendation, the OWASP default recommendation, and what Password Hash Generator defaults to for exactly this reason.
- Argon2i only if you have a concrete reason to worry about side-channel observation specifically — genuinely shared, untrusted hardware where cache-timing is a realistic threat — and you're willing to pay for it with a higher iteration count to keep trade-off resistance up.
- Argon2d only outside the password-storage threat model entirely — proof-of-work, or any context where nothing is watching the memory access pattern from a position to exploit it.
Try it yourself
Password Hash Generator computes real Argon2id, Argon2i, and Argon2d hashes with configurable memory, iterations, and parallelism, so you can see how each parameter actually changes the output and the timing. Password Hash Inspector decodes an existing Argon2 hash back into its variant and every embedded parameter. Both run entirely in your browser.