Authentication endpoints are the front door to every web application. They're also the first target for automated attacks — credential stuffing, brute force, and account enumeration. We decided to add rate limiting to every single auth route in Boottify. All 23 of them.
WHY EVERY ROUTE MATTERS
Most guides only rate-limit /sign-in and /sign-up. But attackers are smarter than that:
- /forgot-password — Can be used for email enumeration and email bombing
- /verify-2fa — Brute-forceable if TOTP codes aren't rate-limited (only 999,999 possibilities)
- /oauth/[provider] — OAuth state generation can be abused for session fixation recon
- /reset-password — Token brute-forcing if tokens are short
- /enable-2fa — Can be called repeatedly to generate TOTP secrets
We identified 23 auth-related API routes that needed protection, each with different risk profiles.
THE 5-TIER SYSTEM
Not all endpoints need the same limits. A login attempt is more sensitive than a session heartbeat. We designed five tiers:
export const rateLimiters = {
// Tier 1: Strictest — login, password reset
strict: { windowMs: 15 * 60 * 1000, max: 5 },
// Tier 2: Auth actions — 2FA setup, OAuth
auth: { windowMs: 15 * 60 * 1000, max: 10 },
// Tier 3: Standard — sign-up, profile updates
standard: { windowMs: 15 * 60 * 1000, max: 20 },
// Tier 4: Relaxed — read-heavy, low risk
relaxed: { windowMs: 15 * 60 * 1000, max: 50 },
// Tier 5: Permissive — heartbeats, public reads
permissive:{ windowMs: 15 * 60 * 1000, max: 100 },
};
Tier Assignment
| Tier | Routes | Max Requests / 15min |
|---|---|---|
| Strict | sign-in, forgot-password, reset-password, verify-2fa | 5 |
| Auth | enable-2fa, disable-2fa, setup-2fa, OAuth callbacks | 10 |
| Standard | sign-up, sign-out, backup-codes | 20 |
| Relaxed | me, providers, 2fa-methods | 50 |
| Permissive | heartbeat, clear-session | 100 |
IN-MEMORY SLIDING WINDOW
We chose an in-memory sliding window algorithm over Redis-based solutions. For a single-server deployment like Boottify, this avoids adding infrastructure while still being accurate.
import { rateLimit } from "@/lib/rate-limit";
export async function POST(req: Request) {
const limited = await rateLimit(req, rateLimiters.strict);
if (limited) return limited; // Returns 429 response
// ... handle sign-in logic
}
The rate limiter returns a Response object with proper headers (Retry-After, X-RateLimit-Remaining) or null if the request is allowed.
TESTING WITH VITEST
We wrote unit tests that verify each tier's behavior — request counting, window expiry, and header correctness:
describe("rate-limit strict tier", () => {
it("allows 5 requests then blocks", async () => {
for (let i = 0; i < 5; i++) {
expect(await rateLimit(mockReq, rateLimiters.strict)).toBeNull();
}
const blocked = await rateLimit(mockReq, rateLimiters.strict);
expect(blocked?.status).toBe(429);
});
});
RESULTS
Since deploying rate limiting across all 23 auth routes:
- 0 successful brute-force attempts (previously saw probing traffic daily)
- Zero false positives from legitimate users (tiers are generous enough for normal usage)
- <1ms overhead per request (in-memory Map lookup)
- 23 routes protected with just 2 lines added to each handler
Rate limiting isn't glamorous, but it's one of the highest-impact security improvements you can make. If you're only protecting login, you're leaving the door open.



