· via dev.to (home feed)
Django 6.1's PBKDF2 bump rewrites password hashes on login, even stronger ones
Django 6.1 raises default PBKDF2 iterations from 1.2M to 1.5M and silently rewrites stored password hashes at login, including downgrading deliberately hardened ones, a dev.to analysis finds.

Django 6.1, which shipped in August, changed one number in django.contrib.auth: the default PBKDF2 iteration count for password hashing rose from 1,200,000 to 1,500,000. A hands-on analysis published on dev.to shows the change reaches further than a slightly slower login. Most notably, Django silently rewrites users' stored password hashes the next time they sign in, and it will weaken hashes that were deliberately hardened above the new default.
What the bump costs
The author installed Django 6.1.2 and 6.0.9 in separate virtualenvs and timed 20 hashes at each version's default iteration count, across three process runs on an idle 4-core container. Mean verify times, the work done on every login, moved from roughly 270–290ms to 366–378ms, and the fastest times scaled by almost exactly the 1.25 ratio between the new and old iteration counts. The net effect is that each login consumes roughly 50–90ms more CPU, depending on how busy the machine is.
That cost does not serialize requests, though. Because CPython's hashlib.pbkdf2_hmac releases the GIL while OpenSSL does the underlying work, the benchmarks showed throughput scaling almost linearly up to four workers, around 10.5–10.7 hashes per second, before flattening at eight on the 4-core box. The dev.to post concludes the price is paid in CPU headroom on login-heavy deployments rather than in a blocked thread per request.
Hashes are rewritten on the next login
Django has a general mechanism for upgrading a stored hash when the current hasher's parameters no longer match what is stored, and the author verified that it fires for this specific change. A user whose hash was created under 1,200,000 iterations logged in once via authenticate(); the stored hash came back at 1,500,000 iterations, with query logging capturing the UPDATE against auth_user. No migration command was run and no explicit set_password() call was made. A second login triggered no further writes, because the update only happens when the stored iteration count disagrees with the live default.
For a site moving from 6.0 to 6.1, that means every active user's hash gets strengthened one login at a time, with no backfill job required.
Stronger hashes get downgraded
The check that decides whether to rewrite is not whether the stored count is lower than the current default, but whether it differs at all. The author tested a hash deliberately set to 3,000,000 iterations, double the new default and the kind of hardening a security-conscious admin might configure through a custom PASSWORD_HASHERS entry. After one login it had been rewritten to 1,500,000. Nothing warned about it, and according to the post, the 6.1 release notes describe the number going up without covering what happens to hashes that were already above it.
No guardrails on the setting
The analysis also probed the edges of the hasher. A negative iteration count raises a ValueError, but an iteration count of 1 is accepted without complaint, producing a hash in about a quarter of a millisecond. There is no system check, and nothing in manage.py check --deploy inspects PASSWORD_HASHERS iteration counts. A separate quirk: encode() resolves its argument as iterations = iterations or self.iterations, so passing 0 explicitly is silently discarded, and Python's truthiness rules hand you the full-strength default instead of honoring or rejecting the input.
None of this is visible from the application's own signals either. The post notes there is no log line and no Django signal that distinguishes a hasher-upgrade save from an ordinary password change; the only trace is the UPDATE query itself.
Why it matters
The silent upgrade is a genuine win for default deployments, since hashing strength improves for active users with zero operational work. But the mechanism cuts both ways, and the any-difference comparison means admins who tuned iterations upward for extra margin will see that hardening walked back, invisibly, one login at a time. Anyone upgrading to 6.1 with a customized hasher should decide what they want the post-upgrade default to be, or audit stored hashes before users sign in. The roughly 25% jump in per-login CPU is also a capacity-planning line item for login-heavy sites, and the absence of any floor or deploy-time check on iteration counts leaves misconfiguration entirely to the operator.
- #django
- #python
- #security
- #password-hashing
- #pbkdf2