Speed up your test suite

Use a fast password hasher in tests

Django hashes passwords with hundreds of thousands of PBKDF2 iterations, and every create_user and client.login in a test pays for it. Switch to a fast hasher in test settings and use force_login.

Password hashing is slow on purpose: it makes stolen password hashes expensive to crack. Django's default hasher, PBKDF2, runs hundreds of thousands of SHA-256 iterations for every password, and the count goes up with every Django release. That takes tens to hundreds of milliseconds of CPU.

In production that cost is paid once per login. In a test suite it's paid every time a test creates a user with a password or logs one in, and in suites with many user-facing tests it's often the single biggest cost: a few hundred tests that each create and log in a user can spend minutes hashing.

Why tests hash so many passwords

Each of these hashes a password:

  • User.objects.create_user(username, password=...) and create_superuser()
  • user.set_password(...) and make_password(...)
  • self.client.login(username=..., password=...), which runs authenticate() and checks the password against its hash
  • factories that call set_password in a post-generation hook, e.g. factory_boy's PostGenerationMethodCall("set_password", ...)
  • loading fixtures through a signup form or API

A test that creates a user and logs them in pays for two hashes. A setUp that does that for 50 tests pays for 100.

The fix: a fast hasher in test settings

Use a cheap hasher only while testing. Django's own documentation recommends MD5PasswordHasher for this:

# settings.py
import sys

TESTING = "test" in sys.argv or "pytest" in sys.modules

if TESTING:
    PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]

If you keep separate test settings (DJANGO_SETTINGS_MODULE=myproject.settings.test), set it there instead.

Log in without a password: force_login

client.login() checks the password, which means hashing it again. Unless the test is about the login form or authentication itself, use force_login(), which attaches the user to the session directly:

# Before: hashes the password twice (create_user and login)
user = User.objects.create_user("alice", password="secret")
self.client.login(username="alice", password="secret")

# After: no hashing at all
user = User.objects.create_user("alice")
self.client.force_login(user)

create_user() without a password sets an unusable password and skips hashing. With pytest-django the admin_client fixture and client.force_login(user) work the same way.

Outside Django

The same idea applies anywhere: use the cheapest work factor your hashing library allows, only in tests.

# bcrypt: 4 is the minimum cost (the default is 12, which is 256 times slower)
bcrypt.hashpw(password, bcrypt.gensalt(rounds=4))

# argon2-cffi
from argon2 import PasswordHasher

hasher = PasswordHasher(time_cost=1, memory_cost=8, parallelism=1)

# passlib
pwd_context = CryptContext(schemes=["bcrypt"], bcrypt__rounds=4)

Make the parameters configurable (a setting or environment variable) so production keeps the strong defaults.

Keep the tests correct

  • Change the hasher in test settings only. A fast hasher in production would make stolen hashes easy to crack.
  • Tests that assert on a specific hash format (pbkdf2_sha256$...) need updating, or an override_settings(PASSWORD_HASHERS=[...]) on that one test.
  • Keep a few tests that go through the real login form with client.login() or a POST, so authentication itself stays covered.

How flakytest finds it

flakytest attributes the time spent in hashing functions (PBKDF2, bcrypt, argon2, scrypt) to a password hashing category. When it's more than about 1% of test time it suggests Use a fast password hasher in tests, shows your current hasher, and lists the lines that call client.login() or authenticate() so you know where force_login() would help.

Further reading

Related guides

  • Create shared test data in setUpTestData instead of setUp: Django runs setUp before every test, so its rows are inserted again each time. setUpTestData creates them once per TestCase class, and every test still starts clean.
  • Faster factory_boy and model_bakery factories: Slow factory_boy and model_bakery factories? Create shared data once, use build() or baker.prepare() instead of saving, bulk create rows, reuse one Faker instance and tune Hypothesis.
  • Django testing best practices: How to write fast, reliable Django tests: TestCase vs TransactionTestCase, setUpTestData, model_bakery, force_login, test settings, assertNumQueries and parallel runs.

Updated October 18, 2026 · Suggest a change