Rate Limit

Better Auth Ruby can rate limit requests before endpoint handlers run.

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  rate_limit: {
    enabled: true
  }
)

By default, general routes use window: 10 and max: 100. Auth-sensitive paths such as /sign-in, /sign-up, /change-password, and /change-email use stricter upstream-compatible rules.

Configuring Rate Limit

Connecting IP Address

Rate limiting keys include the client IP and route path. Configure trusted IP headers when your app runs behind a proxy.

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  advanced: {
    ip_address: {
      ip_address_headers: ["x-forwarded-for"]
    }
  },
  rate_limit: {
    enabled: true
  }
)

Disable IP tracking when another layer handles rate limiting:

advanced: {
  ip_address: {
    disable_ip_tracking: true
  }
}

IPv6 Address Support

IPv6 addresses are normalized before becoming rate-limit keys. IPv4-mapped IPv6 addresses are normalized back to IPv4.

IPv6 Subnet Rate Limiting

Configure IPv6 subnet grouping:

advanced: {
  ip_address: {
    ip_address_headers: ["x-forwarded-for"],
    ipv6_subnet: 64
  }
}

Requests from addresses within the same subnet share a rate-limit key.

Rate Limit Window

Configure a global window and max:

rate_limit: {
  enabled: true,
  window: 60,
  max: 20
}

Use custom route rules:

rate_limit: {
  enabled: true,
  custom_rules: {
    "/magic-link/*" => {window: 60, max: 5},
    "/get-session" => false
  }
}

Custom rules can also be lambdas:

rate_limit: {
  enabled: true,
  custom_rules: {
    "/admin/*" => lambda do |request, current_rule|
      current_rule.merge(max: 10)
    end
  }
}

Storage

By default, rate limit data is stored in memory.

Using Database

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  database: :memory,
  rate_limit: {
    enabled: true,
    storage: "database"
  }
)

Run migrations after enabling database storage so the rateLimit table exists.

Using Secondary Storage

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  secondary_storage: redis_storage,
  rate_limit: {
    enabled: true,
    storage: "secondary-storage"
  }
)

Custom Storage

class RateLimitStorage
  def get(key)
    @store[key]
  end

  def set(key, value, ttl: nil, update: false)
    @store[key] = value
  end
end

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  rate_limit: {
    enabled: true,
    custom_storage: RateLimitStorage.new
  }
)

Handling Rate Limit Errors

When a request exceeds the limit, Better Auth returns 429 and the x-retry-after header.

status, headers, body = auth.call(env)

if status == 429
  retry_after = headers.fetch("x-retry-after").to_i
  payload = JSON.parse(body.join)
end

Server-side API calls that execute through Rack observe the same response behavior.

Schema

When rate_limit.storage is "database", the schema includes the rateLimit table.

FieldTypePurpose
idstringDatabase ID.
keystringUnique rate-limit key.
countnumberNumber of requests in the current window.
lastRequestnumberLast request timestamp in epoch milliseconds.

Customize the model name and fields with the rate_limit schema options before generating migrations.

On this page