Rate Limiting Policy
The exact formula that turns an offline brute-force problem into a bounded, online one.
The formula
-
The first 5 requests in a streak (n = 1..5) are always allowed, free.
-
From the 6th request onward (n ≥ 6):
lockoutSeconds = min(30 days, 30 × 2^(n − 6)) -
24 hours of silence resets the attempt counter to 0 — this is the only unlock mechanism. There is no admin override: anything that could bypass the lockout could also be tricked into leaking the salt.
| Attempt (n) | Lockout |
|---|---|
| 1–5 | allowed (free) |
| 6 | 30s |
| 7 | 60s |
| 8 | 120s |
| 9 | 240s |
| 10 | 480s |
| … | doubling each attempt |
| ~22 | 30 days (cap reached) |
No existence-timing oracle
Both allowed and denied requests increment the counter and advance the timestamp — this
applies identically to unknown, never-provisioned accountIds and to known, locked ones. An
attacker cannot distinguish "this account doesn't exist" from "this account is locked" by
response cost or shape.
Dev-only bypass
A DISABLE_RATE_LIMIT environment variable exists purely for local development —
it disables the gate entirely and persists no state, logging a startup warning when active.
It must never be set in production; doing so removes the only defense against offline PIN
brute force.
Redis vs. sqlite
Locally, this state lives in sqlite behind an atomic transaction. On Vercel, it's Redis-backed via a plain read-then-write (Upstash's REST API has no transaction primitive) — the same pure policy function runs either way, but the Redis path can under-count concurrent hits by one. It can never over-count into a false lockout. See Deployment Architecture for the full picture.