# Rate Limiting

> Protect authentication endpoints from brute force attacks and abuse with rate limiting.

*Canonical: https://makerkit.dev/docs/nextjs-drizzle/better-auth/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).

## 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`:

```typescript
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

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

```bash {% title="apps/web/.env.local" %}
BETTER_AUTH_RATE_LIMIT_STORAGE=secondary-storage
```

This requires configuring Better Auth's secondary storage adapter. See [Better Auth documentation](https://www.better-auth.com/docs/concepts/secondary-storage) for setup instructions.

## Customization

### Stricter Rate Limiting

For applications requiring tighter security, reduce the limits:

```typescript {% title="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:

```typescript
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:

```typescript {% title="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:

```typescript
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:

```typescript
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](../database/rate-limit-service) 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.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Does rate limiting apply to all auth endpoints?", "answer": "Yes. All Better Auth API endpoints are rate limited when enabled. This includes sign-in, sign-up, password reset, and verification endpoints."},
     {"question": "How is the rate limit key determined?", "answer": "By default, Better Auth uses the client IP address. Behind proxies, ensure X-Forwarded-For headers are properly configured."},
     {"question": "Can I whitelist certain IPs?", "answer": "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)."},
     {"question": "What happens when rate limited?", "answer": "Clients receive a 429 Too Many Requests response with a Retry-After header indicating when they can retry."},
     {"question": "Should I use Redis for rate limiting?", "answer": "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 →](./one-time-token-plugin)
