docs/content/docs/concepts/rate-limit.mdx
Better Auth includes a built-in rate limiter to help manage traffic and prevent abuse. By default, in production mode, the rate limiter is set to:
You can easily customize these settings by passing the rateLimit object to the betterAuth function.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
window: 10, // time window in seconds
max: 100, // max requests in the window
},
})
Rate limiting is disabled in development mode by default. In order to enable it, set enabled to true:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
enabled: true,
//...other options
},
})
In addition to the default settings, Better Auth provides custom rules for specific paths. For example:
/sign-in/email: Is limited to 3 requests within 10 seconds.In addition, plugins also define custom rules for specific paths. For example, twoFactor plugin has custom rules:
/two-factor/verify: Is limited to 3 requests within 10 seconds.These custom rules ensure that sensitive operations are protected with stricter limits.
Rate limiting uses the connecting IP address to track the number of requests made by a user. The
default header checked is x-forwarded-for, which is commonly used in production environments. If
you are using a different header to track the user's IP address, you'll need to specify it.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
advanced: {
ipAddress: {
ipAddressHeaders: ["cf-connecting-ip"], // Cloudflare specific header example
},
},
rateLimit: {
enabled: true,
window: 60, // time window in seconds
max: 100, // max requests in the window
},
})
Better Auth automatically normalizes IPv6 addresses to prevent bypass attacks where attackers use different representations of the same IPv6 address (e.g., 2001:db8::1 vs 2001:0db8:0000:0000:0000:0000:0000:0001). This ensures that all representations of the same IPv6 address are treated as the same for rate limiting purposes.
Additionally, IPv4-mapped IPv6 addresses (e.g., ::ffff:192.0.2.1) are automatically converted to their IPv4 form (192.0.2.1) to prevent attackers from bypassing rate limits by switching between IPv4 and IPv6 representations.
By default, IPv6 addresses are rate limited per /64 subnet, not per individual address. ISPs and cloud providers assign IPv6 prefixes (typically /64 for residential users per RFC 6177) rather than single addresses, so any per-address counter would let one client rotate through 2^64 source addresses without exhausting the limit.
You can override the prefix length via the ipv6Subnet option if your deployment needs a different allocation boundary:
export const auth = betterAuth({
//...other options
advanced: {
ipAddress: {
ipv6Subnet: 56, // Rate limit by /56 subnet (residential ISP allocation)
},
},
rateLimit: {
enabled: true,
window: 60,
max: 100,
},
})
Common IPv6 prefix lengths:
128: Individual IPv6 address. Most restrictive. Only safe when you control the network and trust that each address maps to a distinct client.64 (default): /64 subnet. Typical home or business allocation.56: /56 subnet. Residential ISP allocation per RFC 6177.48: /48 subnet. Larger network allocation.32: /32 subnet. ISP-level allocation.Any integer prefix length from 0 to 128 is accepted; the values above are the most common.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
window: 60, // time window in seconds
max: 100, // max requests in the window
},
})
You can also pass custom rules for specific paths.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
window: 60, // time window in seconds
max: 100, // max requests in the window
customRules: {
"/sign-in/email": {
window: 10,
max: 3,
},
"/two-factor/*": async (request)=> {
// custom function to return rate limit window and max
return {
window: 10,
max: 3,
}
}
},
},
})
If you like to disable rate limiting for a specific path, you can set it to false or return false from the custom rule function.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
customRules: {
"/get-session": false,
},
},
})
By default, rate limit data is stored in memory, which may not be suitable for many use cases, particularly in serverless environments. To address this, you can use a database, secondary storage, or custom storage for storing rate limit data.
Using Database
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
storage: "database",
modelName: "rateLimit", //optional by default "rateLimit" is used
},
})
Make sure to run migrate to create the rate limit table in your database:
npx auth@latest migrate
Using Secondary Storage
If a Secondary Storage has been configured you can use that to store rate limit data.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
storage: "secondary-storage"
},
})
Custom Storage
If none of the above solutions suits your use case you can implement a customStorage.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
//...other options
rateLimit: {
customStorage: {
get: async (key) => {
// get rate limit data
},
set: async (key, value) => {
// set rate limit data
},
},
},
})
When a request exceeds the rate limit, Better Auth returns the following header:
X-Retry-After: The number of seconds until the user can make another request.To handle rate limit errors on the client side, you can manage them either globally or on a per-request basis. Since Better Auth clients wrap over Better Fetch, you can pass fetchOptions to handle rate limit errors
Global Handling
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
fetchOptions: {
onError: async (context) => {
const { response } = context;
if (response.status === 429) {
const retryAfter = response.headers.get("X-Retry-After");
console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
}
},
}
})
Per Request Handling
import { authClient } from "./auth-client";
await authClient.signIn.email({
fetchOptions: {
onError: async (context) => {
const { response } = context;
if (response.status === 429) {
const retryAfter = response.headers.get("X-Retry-After");
console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
}
},
}
})
If you are using a database to store rate limit data you need this schema:
Table Name: rateLimit
export const rateLimitTableFields = [ { name: "id", type: "string", description: "Database ID", isPrimaryKey: true, }, { name: "key", type: "string", description: "Unique identifier for each rate limit key", isUnique: true, }, { name: "count", type: "integer", description: "Number of requests made in the current window", }, { name: "lastRequest", type: "bigint", description: "Timestamp of the last request (epoch ms)", }, ];
<DatabaseTable name="rateLimit" fields={rateLimitTableFields} />