Tuning RECHECK and max_age¶
RECHECK_MIN, RECHECK_MAX, and the per-view max_age decorator argument
together control “how long does an MFA verification stay valid?”. This page
helps you pick values for your threat model.
The two knobs¶
MULTIFACTOR["RECHECK_MIN"]/RECHECK_MAX— global. Every factor verification gets a random expiry uniformly in[MIN, MAX]. Default 3h–6h.@multifactor_protected(..., max_age=N)— per-view. Forces a re-challenge if the most recent verification is older thanNseconds. Default0(off).
The two are independent. RECHECK is the floor of MFA freshness across
your whole site; max_age is a ceiling that lets you require stricter
freshness for individual views.
How to choose¶
Application kind |
|
|
|
|---|---|---|---|
Consumer app, low-risk pages |
8h |
24h |
Generally not needed. |
B2B SaaS (typical) |
3h (default) |
6h (default) |
30 min on billing, 10 min on team-admin pages. |
Internal admin console |
1h |
2h |
5 min on destructive actions (delete user, change role). |
Banking / vault |
15 min |
30 min |
60 s on transfer-money; 0 s (always challenge) on add-payee. |
Anything |
normal |
normal |
5 min, so the user is comfortable approving multiple verifications in a session. |
The randomness matters¶
flowchart LR
subgraph WithoutJitter["Without jitter — synchronised peak"]
H0[09:00 login peak] --> H1[Every user verifies at ~09:00]
H1 --> H2[12:00 — every user re-challenged at once]
end
subgraph WithJitter["With jitter — spread peak"]
H3[09:00 login peak] --> H4[Every user verifies at ~09:00]
H4 --> H5[12:00–15:00 — re-challenges spread across 3 hours]
end
style WithoutJitter fill:#fff3cd
style WithJitter fill:#d4edda
Setting RECHECK_MIN = RECHECK_MAX removes the jitter. Don’t do this in
production — you’ll get synchronised re-prompt storms that hammer your
auth path at predictable times.
Anti-patterns¶
RECHECK = Falsein production. Means “once verified, verified for the session”. Combined with long session lifetimes, an MFA verification can effectively last for days. Don’t.RECHECK_MIN = 0. Means users may be re-challenged within seconds. Friction without a clear benefit; you almost certainly want a per-viewmax_ageinstead.Identical
RECHECK_MINandRECHECK_MAX. See above; loses the jitter benefit.max_age = 1. “Force re-challenge on every page” — annoys users enormously. Use it only on truly one-shot actions (large monetary transfers, code execution).max_age = 60on a busy app. A user filling out a form for >1 min will lose their work when bounced through MFA. Use 5+ minutes unless you also implement form-state preservation.
Interaction with session lifetime¶
SESSION_COOKIE_AGE is Django’s session cookie lifetime (default 2 weeks).
A verified MFA session can outlive the recheck window: the session is
still valid, but active_factors() filters out the expired entries and
the user is re-challenged.
Make sure your session lifetime is at least as long as your RECHECK_MAX
— otherwise users will be re-challenged because their session died, which
is fine but confusing.
A senior-dev decision matrix¶
When in doubt:
What’s the worst thing a hijacked session can do? That dictates
RECHECK_MAX— set it shorter than how much damage you can absorb.What action is irreversible in your domain? Add
max_age≤ 5 min on the views that perform it.What’s the typical user session?
RECHECK_MINshould be longer than the median session so genuine users rarely see a mid-session re-prompt.Re-evaluate after rollout. Measure: how often do real users get re-challenged? If it’s many times per day per user, dial back.