Login Rate Limiting Configuration Guide
This document explains how to configure IP-based rate limiting for authentication endpoints in the NEXTY.DEV boilerplate. Rate limiting helps prevent abuse such as email spam, brute force attacks, and excessive use of free tier resources.
Overview
NEXTY.DEV uses Better Auth's built-in rate limiting feature by default, with optional Upstash Redis support for serverless environments.
Default Configuration
rateLimit: {
enabled: true,
window: 60, // 60 seconds
max: 100, // 100 requests per window (global default)
customRules: {
"/sign-in/magic-link": { window: 60, max: 3 },
"/email-otp/send-verification-otp": { window: 60, max: 3 },
"/sign-in/email-otp": { window: 60, max: 5 },
},
}| Endpoint | Window | Max Requests | Description |
|---|---|---|---|
/sign-in/magic-link | 60s | 3 | Magic Link login email requests |
/email-otp/send-verification-otp | 60s | 3 | OTP verification code email requests |
/sign-in/email-otp | 60s | 5 | OTP verification attempts |
| All other endpoints | 60s | 100 | Global default |
Customization Examples
Scenario 1: Stricter Limits to Prevent Free Tier Abuse
If you're concerned about users abusing free tier credits by switching accounts, add stricter daily limits:
rateLimit: {
enabled: true,
window: 60,
max: 100,
customRules: {
// Strict email sending limits
"/sign-in/magic-link": {
window: 60 * 60 * 24, // 24 hours
max: 5, // Only 5 magic links per IP per day
},
"/email-otp/send-verification-otp": {
window: 60 * 60 * 24, // 24 hours
max: 10, // Only 10 OTP codes per IP per day
},
// Keep verification attempts reasonable
"/sign-in/email-otp": {
window: 60 * 60 * 24, // 24 hours
max: 20, // 20 attempts per 24 hours
}
},
...(redis && {
customStorage: {
get: async (key: string) => {
const data = await redis!.get<{ key: string; count: number; lastRequest: number }>(key);
return data || undefined;
},
set: async (key: string, value: { key: string; count: number; lastRequest: number }) => {
await redis!.set(key, value, { ex: 60 * 60 * 24 }); // Auto-expire after 24 hours
},
},
}),
}Scenario 2: Very Strict Anti-Abuse (High-Value Services)
For services where free tier abuse is costly (e.g., AI credits, API calls):
rateLimit: {
enabled: true,
window: 60,
max: 50, // Lower global default
customRules: {
// Extremely strict email limits
"/sign-in/magic-link": {
window: 60 * 60 * 24, // 24 hours
max: 3, // Only 3 magic links per IP per day
},
"/email-otp/send-verification-otp": {
window: 60 * 60 * 24, // 24 hours
max: 5, // Only 5 OTP codes per IP per day
},
// Strict verification limits
"/sign-in/email-otp": {
window: 60 * 60 * 24, // 24 hours
max: 20, // 20 attempts per 24 hours
},
},
...(redis && {
customStorage: {
get: async (key: string) => {
const data = await redis!.get<{ key: string; count: number; lastRequest: number }>(key);
return data || undefined;
},
set: async (key: string, value: { key: string; count: number; lastRequest: number }) => {
await redis!.set(key, value, { ex: 60 * 60 * 24 }); // Auto-expire after 24 hours
},
},
}),
}Scenario 3: Relaxed Limits (Internal Tools / Low Risk)
For internal tools or low-risk applications:
rateLimit: {
enabled: true,
window: 60,
max: 200, // Increase global default
customRules: {
"/sign-in/magic-link": { window: 60, max: 10 },
"/email-otp/send-verification-otp": { window: 60, max: 10 },
"/sign-in/email-otp": { window: 60, max: 20 },
},
}Scenario 4: Disable Rate Limiting (Development Only)
rateLimit: {
enabled: false, // ⚠️ Never do this in production!
}Storage Options
In-Memory Storage (Default)
If you haven't configured Upstash Redis environment variables, the boilerplate's built-in Redis methods won't activate, and rate limit data will be stored in memory.
Pros:
- No external dependencies
- Simple setup
Cons:
- Data lost on server restart
- Cannot be shared across multiple instances. When deployed to serverless environments like Vercel, each instance has its own memory, making rate limiting ineffective
Upstash Redis Storage (Recommended for Production)
The boilerplate automatically uses Redis when UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are configured.
...(redis && {
customStorage: {
get: async (key: string) => {
const data = await redis!.get<{ key: string; count: number; lastRequest: number }>(key);
return data || undefined;
},
set: async (key: string, value: { key: string; count: number; lastRequest: number }) => {
await redis!.set(key, value, { ex: 120 }); // Auto-expire after 120 seconds
},
},
}),Pros:
- Data persists across restarts
- Shared across all instances
- Perfect for serverless environments (Vercel, Cloudflare Workers)
- Automatic data cleanup via TTL
Cons:
- Requires Upstash account (free tier available)
Setting Up Upstash Redis
See Upstash Integration steps.
Database Storage
Good to know
- This approach is not built into the boilerplate. If you want to use this solution, implement it yourself according to the documentation.
- Database storage does not automatically clean up expired records. You need to set up scheduled tasks to clean old records.
Better Auth also supports database storage:
rateLimit: {
enabled: true,
storage: "database",
modelName: "rateLimit", // Table name
// ...
}Then run the migration:
pnpm dlx @better-auth/cli migrate