Rate Limiting That Actually Works
I added rate limiting to Aegis2FA after realizing how easy brute force attacks are. Here's what I learned.
I added rate limiting to Aegis2FA after reading about how trivial brute force attacks are. Someone with a script can try thousands of passwords per minute if you don't stop them.
The Basics
For login endpoints, I use 5 attempts per 15 minutes per IP. After that, you're locked out temporarily.
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // Don't count successful logins
});
app.post('/api/auth/login', authLimiter, loginHandler);The skipSuccessfulRequests part is important. If someone logs in successfully, that shouldn't count against their limit. You only want to track failed attempts.
Use Redis, Not Memory
The default rate limiter stores counts in memory. That breaks if you have multiple server instances or if your server restarts.
I switched to Redis. Now rate limit data persists and works across instances.
import RedisStore from 'rate-limit-redis';
const limiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
windowMs: 15 * 60 * 1000,
max: 5,
});Different Limits for Different Endpoints
Not everything needs the same limits.
- Login: 5 attempts / 15 min (strict, security-critical)
- 2FA verification: 5 attempts / 15 min (same reason)
- Password reset: 3 attempts / hour (prevent email spam)
- Read-only endpoints: 60 requests / minute (more lenient)
The pattern I follow: anything that could be abused for brute force or spam gets strict limits. Read operations get normal limits.
What I'd Add in Production
User-based rate limiting. IP-based is fine for learning, but multiple users behind the same NAT share an IP. In production, I'd track by user ID for authenticated endpoints.
Adaptive limits. If someone hits rate limits repeatedly, their limits should get stricter. If an IP gets blocked 10 times in a day, maybe blacklist it.
Monitoring. I should have added logging from the start. Knowing how often rate limits are hit tells you if your limits are too strict or if you're under attack.
The CSRF Part
I also added CSRF protection while I was thinking about security. For any state-changing endpoint (POST, PUT, DELETE), the frontend needs to include a CSRF token.
This prevents attacks where a malicious site tricks your browser into making requests to my API.
I used the csurf middleware. It's a few lines of setup, but it's one of those things that's easy to forget until it's too late.
Most of my security code in Aegis2FA is boring. That's probably a good sign.