Rate Limiting
Protect authentication endpoints from brute force attacks and abuse with rate limiting.
Protect your authentication endpoints from brute force attacks. Rate limiting restricts how many requests a client can make within a time window, preventing password guessing, credential stuffing, and other automated attacks.
This page is part of the Authentication documentation.
Overview
Rate limiting is automatically applied to all Better Auth API endpoints. When a client exceeds the request limit, they receive a 429 Too Many Requests response and must wait before retrying.
Default Settings
| Setting | Value | Description |
|---|---|---|
| Window | 5 minutes | Time window for counting requests |
| Max Requests | 200 | Maximum requests per window per IP |
| Storage | Database | Where rate limit counters are stored |
These defaults balance security with usability. Most legitimate users won't hit these limits, but automated attacks will be blocked.
Configuration
Rate limiting is configured in packages/better-auth/src/plugins/rate-limit.ts:
import type { BetterAuthRateLimitOptions } from 'better-auth/types';import * as z from 'zod';const WINDOW_SECONDS = 5 * 60; // 5 minutesconst MAX = 200; // 200 requests per windowconst IS_RATE_LIMIT_ENABLED = !process.env.NEXT_PUBLIC_CI;const RATE_LIMIT_STORAGE = z .enum(['database', 'secondary-storage']) .default('database') .parse(process.env.BETTER_AUTH_RATE_LIMIT_STORAGE);function getRateLimitStorageConfig() { if (RATE_LIMIT_STORAGE === 'secondary-storage') { return { storage: RATE_LIMIT_STORAGE }; } return { storage: 'database', modelName: 'rateLimit', };}export const rateLimitConfig = { enabled: IS_RATE_LIMIT_ENABLED, window: WINDOW_SECONDS, max: MAX, ...getRateLimitStorageConfig(),} satisfies BetterAuthRateLimitOptions;Environment Variables
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_CI | Disables rate limiting when set | - |
BETTER_AUTH_RATE_LIMIT_STORAGE | Storage backend: database or secondary-storage | database |
Environment-Based Enablement
Rate limiting behavior by environment:
| Environment | Rate Limiting |
|---|---|
| Production | Enabled |
| Development | Enabled |
CI/CD (NEXT_PUBLIC_CI set) | Disabled |
Rate limiting is disabled in CI to prevent test flakiness. Never disable in production.
Storage Backends
Database (Default)
Uses PostgreSQL via Drizzle ORM. Rate limit counters are stored in the rateLimit table.
Advantages:
- No additional infrastructure
- Works across multiple server instances
- Persists across restarts
Considerations:
- Adds database queries for each request
- May not scale to extremely high traffic
Secondary Storage
For high-traffic applications, you can use Redis or Upstash for rate limit storage:
apps/web/.env.local
BETTER_AUTH_RATE_LIMIT_STORAGE=secondary-storageThis requires configuring Better Auth's secondary storage adapter. See Better Auth documentation for setup instructions.
Customization
Stricter Rate Limiting
For applications requiring tighter security, reduce the limits:
packages/better-auth/src/plugins/rate-limit.ts
const WINDOW_SECONDS = 1 * 60; // 1 minuteconst MAX = 50; // 50 requests per minutePer-Endpoint Limits
Better Auth allows different limits for different endpoints. Modify the configuration to specify endpoint-specific limits:
export const rateLimitConfig = { enabled: IS_RATE_LIMIT_ENABLED, window: WINDOW_SECONDS, max: MAX, customRules: [ { path: '/sign-in', max: 10, window: 60, // 10 attempts per minute for sign-in }, { path: '/forgot-password', max: 5, window: 300, // 5 attempts per 5 minutes for password reset }, ], ...getRateLimitStorageConfig(),};Integration with Better Auth
The rate limit config is applied in the main auth configuration:
packages/better-auth/src/auth.ts
import { rateLimitConfig } from './plugins/rate-limit';export const auth = betterAuth({ // ... other config rateLimit: rateLimitConfig,});Client-Side Handling
When rate limited, handle the 429 response gracefully:
import { authClient } from '@kit/better-auth/client';const result = await authClient.signIn.email({ email: 'user@example.com', password: 'password',});if (result.error?.status === 429) { // Show user-friendly message console.error('Too many attempts. Please wait before trying again.');}Testing
Rate limiting is automatically disabled when NEXT_PUBLIC_CI=true is set, preventing test flakiness from rate limit blocks.
For local testing of rate limiting behavior:
- Temporarily lower limits in rate-limit.ts
- Make rapid requests to trigger the limit
- Verify 429 responses are returned
- Restore original limits
Monitoring
Monitor rate limit events in your application logs. Key metrics to track:
- Number of 429 responses
- IPs/clients hitting rate limits
- Patterns indicating attacks vs legitimate traffic spikes
Custom Rate Limiting for Other Endpoints
For rate limiting outside Better Auth endpoints (API routes, server actions, file uploads), use the database-backed rate limit service:
import { createRateLimitService } from '@kit/database/rate-limit';const rateLimitService = createRateLimitService();// In your API route or server actionconst result = await rateLimitService.limit('api:upload:user123', { windowSeconds: 60, max: 10,});if (!result.success) { return new Response('Rate limited', { status: 429 });}// Proceed with the requestSee the Rate Limit Service Documentation for detailed usage.
Common Pitfalls
- Disabling in production: Never set
NEXT_PUBLIC_CIin production. This disables rate limiting entirely. - Limits too strict: Overly aggressive limits frustrate legitimate users. Monitor and adjust based on real traffic patterns.
- Limits too loose: Default limits are conservative. Tighten for sensitive endpoints like sign-in.
- Not handling 429 in UI: Always show user-friendly messages when rate limited, not technical errors.
Frequently Asked Questions
Does rate limiting apply to all auth endpoints?
How is the rate limit key determined?
Can I whitelist certain IPs?
What happens when rate limited?
Should I use Redis for rate limiting?
Next: One-Time Token Plugin →