Switch Django to Argon2 Password Hashing

Upgrade Django password hashing to Argon2id: install argon2-cffi, put Argon2PasswordHasher first, let existing hashes upgrade on login, wrap old hashes to upgrade everyone now, tune memory and time cost, and size servers for login load.

Problem statement

Django's default password hasher, PBKDF2 with SHA-256 and a high iteration count, is solid. Argon2id, the winner of the Password Hashing Competition, is memory-hard, which makes cracking stolen hashes with GPUs and specialised hardware much more expensive. Django supports Argon2 natively once the argon2-cffi package is installed. Switching is low risk because Django upgrades hashes transparently at login, but you should understand the CPU and memory cost per login, and how to handle users who rarely log in.

Moving to Argon2idAdd argon2-cffidependencyArgon2 firstin PASSWORD_HASHERSUsers log inhash upgradedOptional wrapupgrade old hashes nowNew and returning users get Argon2 hashes automatically; old hashes stay valid until upgraded.
  1. pip install argon2-cffi (or django[argon2]).
  2. Put django.contrib.auth.hashers.Argon2PasswordHasher first in PASSWORD_HASHERS, keeping the others below it.
  3. Existing PBKDF2 hashes keep working and are re-hashed with Argon2 on each user's next successful login.
  4. For users who never log in, optionally wrap old hashes with a custom hasher and a data migration.
  5. Tune time_cost, memory_cost, and parallelism by subclassing only if the defaults are too slow or too fast on your servers.
  6. Rate-limit login endpoints; slower hashing makes floods of login attempts more expensive for you too.

Step-by-step solution

1. Install and configure

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
    "django.contrib.auth.hashers.ScryptPasswordHasher",
]

The first entry is used for new hashes. The rest verify existing hashes. Keep every hasher that might appear in your database in the list, or those users cannot log in.

2. Upgrades on login

When a user logs in with a password stored under a non-preferred hasher, Django verifies it, then re-hashes it with Argon2 and saves it. Check progress:

from django.contrib.auth import get_user_model
from collections import Counter
Counter(u.password.split("$", 1)[0] for u in get_user_model().objects.only("password").iterator())
hash migration after the switchDeploy Argon2 firstday 0New signupsArgon2 at onceActive users log inupgraded on loginDormant accountsstill PBKDF2Optional wrapmigrationeveryone protectedLogin-time upgrades cover active users; wrapping covers the restdjango-deployment.com
Track the share of hashes by algorithm to see progress.

3. Upgrade dormant accounts now (optional)

Inactive users keep old hashes indefinitely. Django's documentation describes a "wrapped" hasher for this case: a data migration hashes each stored PBKDF2 hash again with the new algorithm and saves it under a custom algorithm name, and the custom hasher's verify() repeats both steps (PBKDF2, then the new algorithm) when the user logs in. Once a wrapped user logs in, Django re-hashes their password directly with the preferred hasher.

Follow the "Password upgrading without requiring a login" section of the Django documentation exactly and test it against a copy of production data, because a mistake locks users out. For many sites, upgrading on login plus forcing a password reset for accounts dormant for years is simpler and just as effective.

4. Tune cost parameters

Django's Argon2 defaults follow current recommendations. To measure:

import timeit
from django.contrib.auth.hashers import make_password
print(timeit.timeit(lambda: make_password("correct horse battery staple"), number=10) / 10)

Aim for roughly 50-250 ms per hash on production hardware. Adjust by subclassing:

class TunedArgon2Hasher(Argon2PasswordHasher):
    time_cost = 3
    memory_cost = 65536   # KiB, 64 MiB
    parallelism = 2

Memory cost multiplies by concurrent logins: 20 simultaneous logins at 64 MiB need over 1 GB. Size workers and memory accordingly.

5. Protect the login endpoint

Expensive hashing makes each login attempt cost you CPU and memory. Rate limits (edge, Nginx, or django-axes) prevent attackers from turning your hasher into a denial-of-service lever.

Argon2 rolloutargon2-cffi in production requirementsbinary wheelArgon2 first, old hashers keptexisting users can log inHash time measured on prod hardware50-250 ms targetMemory sized for concurrent loginsmemory_cost x loginsLogin endpoints rate-limitedno hashing DoSRemoving old hashers from the list is the mistake that locks users outdjango-deployment.com
Tests can use a fast hasher (MD5) in test settings to stay quick.

Verification check: create a user and confirm their password field starts with argon2$argon2id$; log in as an existing PBKDF2 user and confirm the hash changes to Argon2; a load test of the login endpoint stays within memory limits.

Explanation

Password hashes protect users if the database leaks. PBKDF2's cost is purely computational, which GPUs and ASICs parallelise well. Argon2id adds a configurable memory requirement per hash, so attackers need proportionally more memory per guess, which limits parallel cracking. Django stores the algorithm and parameters inside each hash string, so multiple algorithms coexist and hashes can be upgraded individually when users authenticate. That design makes the switch safe and gradual.

Edge cases / notes

  • Tests should set PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] in test settings for speed; never in production.
  • External systems that verify Django hashes (for example a legacy service) must support Argon2 before you switch.
  • scrypt is also memory-hard and supported by Django; Argon2id is the more common choice.
  • Django upgrades raise default work factors over time; upgrades on login pick those up automatically.

Next steps

FAQ

How do I enable Argon2 in Django?

Install argon2-cffi and put Argon2PasswordHasher first in PASSWORD_HASHERS.

Will switching hashers log users out or break logins?

No, as long as the old hashers stay in the list. Hashes upgrade on each user's next login.

Is Argon2 better than PBKDF2?

Argon2id is memory-hard, which makes offline cracking more expensive. PBKDF2 remains acceptable with high iteration counts.

Does Argon2 slow down my site?

Only logins and password changes. Measure hash time and rate-limit login endpoints.

Keep going