Ory Kratos includes built-in rate limiting to protect against brute force attacks, credential stuffing, and API abuse.
Rate limit buckets
Kratos uses multiple rate limit buckets with different thresholds based on the endpoint’s sensitivity:
Low
60 requests/minuteFor high-frequency operations like session checks
Medium
30 requests/minuteFor standard self-service operations
High
10 requests/minuteFor sensitive operations like admin API
Endpoint rate limits
Public API
GET /sessions/whoami
GET /sessions
GET /.well-known/ory/webauthn.js
These endpoints are called frequently and have higher limits.GET /self-service/login/browser
GET /self-service/login/api
POST /self-service/login
GET /self-service/registration/browser
POST /self-service/registration
GET /self-service/recovery/browser
POST /self-service/recovery
GET /self-service/verification/browser
POST /self-service/verification
GET /self-service/settings/browser
POST /self-service/settings
GET /self-service/logout/browser
DELETE /sessions/{id}
Admin API
All Admin API endpoints use the high sensitivity bucket (10 requests/minute):
POST /admin/identities
GET /admin/identities
DELETE /admin/identities/{id}
GET /admin/sessions
...
Kratos includes rate limit information in response headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1640000000
Maximum requests allowed in the time window
Number of requests remaining in current window
Unix timestamp when the rate limit resets
Rate limit responses
When a rate limit is exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000042
{
"error": {
"id": "rate_limit_exceeded",
"code": 429,
"message": "Rate limit exceeded. Please try again in 42 seconds.",
"reason": "Too many requests"
}
}
Seconds to wait before retrying
Client-side handling
JavaScript example
async function makeRequest(url, options = {}) {
const response = await fetch(url, {
...options,
credentials: 'include'
});
// Check rate limit headers
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const reset = parseInt(response.headers.get('X-RateLimit-Reset'));
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After'));
console.warn(`Rate limited. Retry after ${retryAfter} seconds`);
// Wait and retry
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return makeRequest(url, options);
}
// Warn when approaching limit
if (remaining < 5) {
console.warn(`Approaching rate limit. ${remaining} requests remaining`);
}
return response;
}
// Usage
const response = await makeRequest('/self-service/login/browser');
Exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
// Exponential backoff: 2^i seconds
const delay = Math.pow(2, i) * 1000;
console.log(`Rate limited. Retrying in ${delay/1000}s...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}
Additional protection
Reverse proxy rate limiting
Add additional rate limiting at the reverse proxy level:
http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=kratos_public:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=kratos_admin:10m rate=1r/s;
server {
# Public API
location / {
limit_req zone=kratos_public burst=20 nodelay;
limit_req_status 429;
proxy_pass http://kratos:4433;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Admin API
location /admin {
limit_req zone=kratos_admin burst=5 nodelay;
limit_req_status 429;
# Only allow from internal network
allow 10.0.0.0/8;
deny all;
proxy_pass http://kratos:4434;
}
}
}
<IfModule mod_ratelimit.c>
<Location "/">
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 10
</Location>
<Location "/admin">
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 1
Require ip 10.0.0.0/8
</Location>
</IfModule>
http:
middlewares:
kratos-ratelimit:
rateLimit:
average: 10
burst: 20
period: 1s
kratos-admin-ratelimit:
rateLimit:
average: 1
burst: 5
period: 1s
routers:
kratos-public:
rule: "Host(`auth.example.com`)"
middlewares:
- kratos-ratelimit
service: kratos-public
kratos-admin:
rule: "Host(`admin.example.com`)"
middlewares:
- kratos-admin-ratelimit
service: kratos-admin
WAF and DDoS protection
Use a Web Application Firewall for advanced protection:
Cloudflare
- Rate limiting rules
- Bot detection
- DDoS protection
- Challenge pages
AWS WAF
- IP rate limiting
- Geo-blocking
- Bot control
- Custom rules
Brute force protection
Account lockout
Implement account lockout after failed attempts:
selfservice:
flows:
login:
after:
password:
hooks:
- hook: revoke_active_sessions
CAPTCHA integration
Add CAPTCHA for suspicious activity:
// Detect rate limit and show CAPTCHA
if (response.status === 429 || failedAttempts > 3) {
await showCaptcha();
}
Popular CAPTCHA solutions:
- reCAPTCHA v3 (Google)
- hCaptcha
- Cloudflare Turnstile
- FriendlyCaptcha
Monitoring
Track rate limit events
Monitor rate limit metrics:
# Prometheus metrics (if enabled)
kratos_http_requests_total{status="429"}
kratos_http_request_duration_seconds{status="429"}
Alert on suspicious activity
# Example Prometheus alert
groups:
- name: kratos_security
rules:
- alert: HighRateLimitHitRate
expr: rate(kratos_http_requests_total{status="429"}[5m]) > 10
for: 5m
annotations:
summary: "High rate limit hit rate detected"
description: "{{ $value }} rate limit errors per second"
Log analysis
Analyze logs for patterns:
# Find IPs hitting rate limits
grep '"status":429' kratos.log | jq -r '.remote_addr' | sort | uniq -c | sort -rn
# Top rate-limited endpoints
grep '"status":429' kratos.log | jq -r '.path' | sort | uniq -c | sort -rn
IP allowlisting
Exempt trusted IPs from rate limiting (configure at reverse proxy level):
geo $limit {
default 1;
10.0.0.0/8 0; # Internal network
203.0.113.0/24 0; # Office IP range
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=kratos:10m rate=10r/s;
Best practices
Respect rate limits
Check X-RateLimit-* headers and implement exponential backoff in clients.
Layer defenses
Combine Kratos rate limiting with reverse proxy limits and WAF rules.
Monitor and alert
Track rate limit hits and investigate unusual patterns.
Document limits
Clearly communicate rate limits to API consumers.
Protect admin API
Use stricter limits and IP restrictions for admin endpoints.
Troubleshooting
Legitimate users hitting rate limits
Solutions:
- Increase limits at reverse proxy level for trusted IPs
- Implement caching for frequently accessed data
- Optimize client-side code to reduce requests
- Use batch operations where possible
Check:
- Kratos is receiving correct client IP (check
X-Forwarded-For)
- No reverse proxy caching bypassing rate limits
- Clock synchronization (rate limit windows are time-based)
False positives from shared IPs
Solutions:
- Use user-based rate limiting (requires authentication)
- Implement CAPTCHA instead of hard blocks
- Allowlist known shared IP ranges (offices, VPNs)
Next steps
Best practices
Complete security hardening guide
Encryption
Learn about data encryption