Argon2 is actually faster than bcrypt on modern hardware, despite its stronger security guarantees.

Let’s see what that looks like in practice. Imagine we’re hashing a password, say "mysecretpassword", on a server.

import argon2
import bcrypt
import time

password = b"mysecretpassword"

# Argon2
start_time = time.time()
hasher_argon2 = argon2.PasswordHasher(
    memory_cost=102400,  # 100 MB
    time_cost=2,        # 2 iterations
    parallelism=8,      # 8 threads
    hash_len=32,        # 32 bytes hash
    salt_len=16         # 16 bytes salt
)
argon2_hash = hasher_argon2.hash(password)
end_time = time.time()
print(f"Argon2 hashing took: {end_time - start_time:.6f} seconds")
print(f"Argon2 hash: {argon2_hash}")

# bcrypt
start_time = time.time()
salt_bcrypt = bcrypt.gensalt(rounds=12)
bcrypt_hash = bcrypt.hashpw(password, salt_bcrypt)
end_time = time.time()
print(f"bcrypt hashing took: {end_time - start_time:.6f} seconds")
print(f"bcrypt hash: {bcrypt_hash}")

Output might look something like this:

Argon2 hashing took: 0.002154 seconds
Argon2 hash: $argon2id$v=19$m=102400,t=2,p=8$ABCDEF...XYZ$
bcrypt hashing took: 0.009876 seconds
bcrypt hash: b'$2b$12$QRSTUVWXYZabcdefghijklmno...'

Notice how Argon2, even with substantial resource costs, can often complete faster than bcrypt. This is because Argon2 was designed to be highly parallelizable and can take advantage of multi-core processors more effectively.

The fundamental problem password hashing algorithms solve is making it computationally expensive to crack passwords, even if an attacker gets their hands on your database. If an attacker has a list of hashed passwords, they want to be able to try guessing passwords very quickly. Password hashing algorithms deliberately slow this process down by requiring significant computational resources (CPU time, memory, or both) for each hash.

bcrypt, introduced in 1999, uses a modified Blowfish cipher and a work factor (rounds) to control its computational cost. The rounds parameter, like 12 in the example, doubles the number of underlying encryption rounds for each increment. Increasing rounds makes hashing slower, and thus cracking harder. However, bcrypt’s design is primarily CPU-bound and doesn’t scale as well with parallel processing compared to newer algorithms.

Argon2, the winner of the Password Hashing Competition in 2015, offers more knobs to turn for controlling its resource usage: memory_cost, time_cost, and parallelism.

  • memory_cost: The amount of RAM the algorithm will use. This makes it resistant to hardware-accelerated attacks using GPUs or ASICs, which often have limited memory.
  • time_cost: The number of iterations (like bcrypt’s rounds). This is the primary CPU-bound factor.
  • parallelism: The number of threads the algorithm can use. This allows it to leverage modern multi-core CPUs.

Argon2 also comes in three variants:

  • Argon2d: Maximizes resistance to GPU cracking but is vulnerable to side-channel attacks.
  • Argon2i: Resistant to side-channel attacks but less resistant to GPU cracking.
  • Argon2id: A hybrid that provides good resistance against both GPU cracking and side-channel attacks. This is the recommended default.

The key levers you control are the parameters for memory, time, and parallelism. For Argon2id, a common starting point for web applications might be memory_cost=102400 (100MB), time_cost=2, and parallelism=8. For bcrypt, rounds=12 is a reasonable default, though many systems use 14 or higher for increased security. The exact values depend on your server’s capacity and how much latency you can tolerate for login operations. You want to set these parameters high enough that cracking takes a long time, but low enough that your users don’t experience noticeable delays when logging in.

When verifying a password with Argon2, you pass the user-provided password and the stored hash to the verify method. The library extracts the parameters from the hash itself to perform the verification using the same settings.

try:
    hasher_argon2.verify(argon2_hash, b"mysecretpassword")
    print("Argon2 verification successful!")
except argon2.exceptions.VerifyMismatchError:
    print("Argon2 verification failed.")

try:
    bcrypt.checkpw(b"mysecretpassword", bcrypt_hash)
    print("bcrypt verification successful!")
except ValueError:
    print("bcrypt verification failed.")

The most surprising aspect of Argon2’s design is how it uses a "data-dependent memory access pattern" in its Argon2d variant. This means the memory locations it reads from and writes to are determined by the input data itself, making it much harder for an attacker to predict memory access patterns and exploit them for faster cracking on specialized hardware. This is a subtle but powerful defense mechanism that goes beyond simply increasing computational work.

Once you’ve mastered Argon2’s tunable parameters, your next challenge will be managing key rotation and securely migrating users to updated hashing algorithms without compromising security.

Want structured learning?

Take the full Cryptography course →