Rate Limit

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:

  • Window: 60 seconds
  • Max Requests: 100 requests

Server-side requests made using auth.api aren't affected by rate limiting. Rate limits only apply to client-initiated requests.

You can easily customize these settings by passing the rateLimit object to the betterAuth function.

auth.ts
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:

auth.ts
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.

Configuring Rate Limit

Connecting IP Address

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.

auth.ts
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
    },
})

IPv6 Address Support

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.

IPv6 Subnet Rate Limiting

By default, IPv6 addresses are rate limited individually (using the full /128 address). However, since IPv6 typically allocates large address blocks to single users, attackers could potentially bypass rate limits by rotating through multiple IPv6 addresses from their allocation.

To prevent this, you can configure rate limiting to apply to IPv6 subnets instead of individual addresses:

auth.ts
export const auth = betterAuth({
    //...other options
    advanced: {
        ipAddress: {
            ipv6Subnet: 64, // Rate limit by /64 subnet instead of individual addresses
        },
    },
    rateLimit: {
        enabled: true,
        window: 60,
        max: 100,
    },
})

Common IPv6 subnet prefix lengths:

  • 128 (default): Individual IPv6 address - most restrictive
  • 64: /64 subnet - typical home/business allocation
  • 48: /48 subnet - larger network allocation
  • 32: /32 subnet - ISP-level allocation

IPv6 subnet configuration only affects IPv6 addresses. IPv4 addresses are always rate limited individually.

Rate Limit Window

auth.ts
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.

auth.ts
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.

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    //...other options
    rateLimit: {
        customRules: {
            "/get-session": false,
        },
    },
})

Storage

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

auth.ts
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 @better-auth/cli migrate

Using Secondary Storage

If a Secondary Storage has been configured you can use that to store rate limit data.

auth.ts
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.

auth.ts
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
            },
        },
    },
})

Handling Rate Limit Errors

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

auth-client.ts
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

auth-client.ts
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`);
            }
        },
    }
})

Schema

If you are using a database to store rate limit data you need this schema:

Table Name: rateLimit

Field NameTypeKeyDescription
idstringDatabase ID
keystring-Unique identifier for each rate limit key
countinteger-Time window in seconds
lastRequestbigint-Max requests in the window

On this page