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)
endServer-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.
| Field | Type | Purpose |
|---|---|---|
id | string | Database ID. |
key | string | Unique rate-limit key. |
count | number | Number of requests in the current window. |
lastRequest | number | Last request timestamp in epoch milliseconds. |
Customize the model name and fields with the rate_limit schema options before generating migrations.