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

SettingValueDescription
Window5 minutesTime window for counting requests
Max Requests200Maximum requests per window per IP
StorageDatabaseWhere 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 minutes
const MAX = 200; // 200 requests per window
const 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

VariableDescriptionDefault
NEXT_PUBLIC_CIDisables rate limiting when set-
BETTER_AUTH_RATE_LIMIT_STORAGEStorage backend: database or secondary-storagedatabase

Environment-Based Enablement

Rate limiting behavior by environment:

EnvironmentRate Limiting
ProductionEnabled
DevelopmentEnabled
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-storage

This 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 minute
const MAX = 50; // 50 requests per minute

Per-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:

  1. Temporarily lower limits in rate-limit.ts
  2. Make rapid requests to trigger the limit
  3. Verify 429 responses are returned
  4. 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 action
const result = await rateLimitService.limit('api:upload:user123', {
windowSeconds: 60,
max: 10,
});
if (!result.success) {
return new Response('Rate limited', { status: 429 });
}
// Proceed with the request

See the Rate Limit Service Documentation for detailed usage.

Common Pitfalls

  • Disabling in production: Never set NEXT_PUBLIC_CI in 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?
Yes. All Better Auth API endpoints are rate limited when enabled. This includes sign-in, sign-up, password reset, and verification endpoints.
How is the rate limit key determined?
By default, Better Auth uses the client IP address. Behind proxies, ensure X-Forwarded-For headers are properly configured.
Can I whitelist certain IPs?
Better Auth doesn't have built-in IP whitelisting. You'd need to implement this at the infrastructure level (e.g., in your reverse proxy).
What happens when rate limited?
Clients receive a 429 Too Many Requests response with a Retry-After header indicating when they can retry.
Should I use Redis for rate limiting?
Database storage works well for most applications. Consider Redis/Upstash for very high traffic or when you need sub-millisecond response times.

Next: One-Time Token Plugin →