Ohne Rate Limiting ist jede API ein DoS-Angriff entfernt vom Ausfall. Aber zu aggressive Limits frustrieren legitime Clients. Die Kunst liegt darin, Schutz und Nutzbarkeit zu balancieren.
Dieser Artikel zeigt, wie du Rate Limits und Quotas sinnvoll designst, klar kommunizierst und fair durchsetzt.
Zielbild
Nach diesem Artikel kannst du:
- Rate Limits von Quotas unterscheiden und beide sinnvoll einsetzen
- Limit-Dimensionen wählen (User, IP, Tenant, Endpoint)
- Limits über Standard-Header kommunizieren
- Burst-Traffic erlauben, ohne das System zu gefährden
- Abuse-Handling implementieren
Kernfragen
Bevor du weiterliest, versuche diese Fragen für dein Projekt zu beantworten:
- Rate Limit oder Quota? Kurzfristig (pro Sekunde) oder langfristig (pro Monat)?
- Welche Dimension? Pro User? Pro IP? Pro Tenant? Pro API Key?
- Welche Endpoints brauchen eigene Limits? Login? Search? Export?
- Wie kommuniziert ihr Limits? Header? Dokumentation?
- Was passiert bei Überschreitung? Block? Queue? Degraded Service?
Rate Limits vs. Quotas
Beide begrenzen Nutzung, aber auf unterschiedlichen Zeitskalen.
Rate Limits
Kurzfristige Begrenzung: Requests pro Sekunde/Minute.
100 requests / minute / user
Zweck:
- Server vor Überlastung schützen
- Faire Verteilung der Ressourcen
- Brute-Force-Angriffe verhindern
Quotas
Langfristige Begrenzung: Requests pro Tag/Monat.
10.000 requests / day / API key
1.000.000 requests / month / tenant
Zweck:
- Business-Modell durchsetzen (Free vs. Paid)
- Kosten kontrollieren
- Ressourcenplanung ermöglichen
Wann was?
| Szenario | Rate Limit | Quota |
|---|---|---|
| DoS-Schutz | Ja | Nein |
| Brute-Force-Schutz | Ja | Nein |
| Tier-basierte Pläne | Optional | Ja |
| Kosten-Kontrolle | Nein | Ja |
| Fair Use | Ja | Ja |
Empfehlung: Beide kombinieren. Rate Limits für kurzfristigen Schutz, Quotas für langfristige Kontrolle.
Limit-Dimensionen
Nach welchem Kriterium wird limitiert?
Dimensionen im Überblick
| Dimension | Identifikation | Use Case |
|---|---|---|
| IP-Adresse | Request IP | Unauthentifizierte Requests |
| User | User ID aus Token | Authentifizierte Requests |
| API Key | Key aus Header | Partner-Integrationen |
| Tenant | Tenant ID aus Token | Multi-Tenant SaaS |
| Endpoint | Request Path | Teure Operationen |
| Global | Keine | Systemschutz |
Kombination von Dimensionen
Oft kombiniert man mehrere:
Global: 10.000 req/s (Systemschutz)
Tenant: 1.000 req/s (Fair Use)
User: 100 req/s (Einzelnutzer)
Endpoint: 10 req/s für /search (teure Operation)
Alle Limits werden geprüft, das niedrigste greift.
IP-basierte Limits
# Einfaches IP-Limit
@rate_limit(limit=100, period="1m", key=lambda req: req.ip)
def handle_request(request):
return {"status": "ok"}
Probleme:
- Shared IPs (NAT, Proxies) → viele User hinter einer IP
- IPv6 → User kann IP wechseln
- VPNs → legitime User werden geblockt
Lösung: IP-Limits nur als Fallback, primär User/API Key nutzen.
User-basierte Limits
@rate_limit(limit=100, period="1m", key=lambda req: req.user.id)
def handle_request(request):
return {"status": "ok"}
Vorteile:
- Fair pro User
- Unabhängig von IP
Nachteile:
- Nur für authentifizierte Requests
- User kann mehrere Accounts erstellen
Tenant-basierte Limits
@rate_limit(limit=1000, period="1m", key=lambda req: req.user.tenant_id)
def handle_request(request):
...
Use Case: SaaS mit Teams. Alle User eines Tenants teilen sich das Limit.
Endpoint-spezifische Limits
Manche Endpoints sind teurer als andere:
| Endpoint | Limit | Begründung |
|---|---|---|
GET /users |
100/min | Standard |
POST /search |
10/min | Teure Query |
POST /export |
5/hour | Sehr teuer |
POST /auth/login |
5/min | Brute-Force-Schutz |
POST /auth/password-reset |
3/hour | E-Mail-Spam |
Rate-Limit-Algorithmen
Verschiedene Algorithmen haben unterschiedliche Eigenschaften.
Fixed Window
Zählt Requests in festen Zeitfenstern.
Window: 00:00 - 01:00
Requests: [################] 100/100
Window: 01:00 - 02:00
Requests: [## ] 12/100
Vorteile:
- Einfach zu implementieren
- Wenig Speicher
Nachteile:
- Burst am Fensterrand: 100 um 00:59, 100 um 01:01 → 200 in 2 Minuten
Sliding Window
Zählt Requests in einem rollierenden Fenster.
Now: 01:30
Window: 00:30 - 01:30 (letzte 60 Minuten)
Vorteile:
- Gleichmäßigere Verteilung
- Kein Burst am Fensterrand
Nachteile:
- Mehr Speicher (Timestamps speichern)
- Komplexer
Token Bucket
Bucket mit Tokens, die regelmäßig aufgefüllt werden.
Bucket: 100 tokens
Refill: 10 tokens/second
Request: -1 token
Burst: Bis zu 100 Requests sofort
Sustained: 10 Requests/Sekunde
Vorteile:
- Erlaubt Burst
- Glättet Traffic langfristig
Nachteile:
- Komplexer zu verstehen
- Burst kann Server kurz überlasten
Leaky Bucket
Requests fließen mit konstanter Rate ab.
Vorteile:
- Konstante Verarbeitungsrate
- Vorhersagbare Last
Nachteile:
- Latenz bei vollem Bucket
- Requests werden verzögert, nicht abgelehnt
Welchen Algorithmus?
| Anforderung | Algorithmus |
|---|---|
| Einfach, gut genug | Fixed Window |
| Kein Burst am Rand | Sliding Window |
| Burst erlauben | Token Bucket |
| Konstante Last | Leaky Bucket |
Empfehlung: Token Bucket für die meisten APIs. Erlaubt Burst, schützt langfristig.
Response Headers
Clients müssen wissen, wie viele Requests sie noch haben.
Standard-Header (IETF Internet-Draft, Stand 27. September 2025)
HTTP/1.1 200 OK
RateLimit-Policy: "user";q=100;w=60
RateLimit: "user";r=42;t=30
| Header | Bedeutung |
|---|---|
RateLimit-Policy |
Policy-Definition (q=Quota/Limit, w=Window in Sekunden) |
RateLimit |
Aktueller Status (r=Remaining, t=Reset in Sekunden) |
Der aktuelle IETF-Entwurf draft-ietf-httpapi-ratelimit-headers-10
definiert diese Felder; es ist ein Internet-Draft (noch kein finaler RFC).
Die Policy-ID (z. B. "user") muss in beiden Feldern übereinstimmen.
Mehrere Policies werden als kommaseparierte Liste in RateLimit-Policy
übertragen. RateLimit kann die gerade kritischste Policy liefern oder
mehrere Items enthalten.
Hinweis: X-RateLimit-* ist verbreitet (Legacy). Wenn du es nutzt,
dokumentiere die Abweichung klar.
Bei Überschreitung
HTTP/1.1 429 Too Many Requests
RateLimit-Policy: "user";q=100;w=60
RateLimit: "user";r=0;t=30
Retry-After: 30
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Please wait before retrying.",
"error_code": "RATE_LIMIT_EXCEEDED",
"request_id": "req_abc123"
}
Retry-After Header
Sagt dem Client, wie lange er warten soll:
Retry-After: 30
Oder als Datum:
Retry-After: Thu, 29 Jan 2026 12:00:00 GMT
Empfehlung: Sekunden (Integer) ist einfacher für Clients.
Quotas über RateLimit-Header
Für langfristige Quotas:
RateLimit-Policy: "monthly";q=10000;w=2592000
RateLimit: "monthly";r=7523;t=86400
Tipp: Wenn du Rate Limits und Quotas parallel kommunizieren willst, nutze mehrere Policies:
RateLimit-Policy: "burst";q=100;w=60, "monthly";q=10000;w=2592000
RateLimit: "monthly";r=7523;t=86400
Burst vs. Sustained
Token Bucket erlaubt Burst, aber wie viel?
Konfiguration
Rate: 10 req/s (sustained)
Burst: 100 req (max Bucket Size)
→ Client kann 100 Requests sofort machen, dann 10/s.
Wann Burst erlauben?
| Szenario | Burst? | Begründung |
|---|---|---|
| Seitenaufbau (viele Requests) | Ja | Bessere UX |
| Batch-Import | Ja | Effizienter |
| Login/Auth | Nein | Brute-Force-Risiko |
| Search | Begrenzt | Teuer, aber Burst OK |
| Export | Nein | Sehr teuer |
Dokumentation
Dokumentiere Limits pro Plan transparent, damit Clients die Grenzen einschätzen und Backoff-Strategien sauber implementieren können.
Beispiel: Rate Limits pro Plan:
| Plan | Rate | Burst |
|---|---|---|
| Free | 10 req/s | 50 req |
| Pro | 100 req/s | 500 req |
| Enterprise | 1000 req/s | 5000 req |
Burst erlaubt kurze Spitzen, danach gilt die Rate.
Quota-Tiers
Quotas ermöglichen Business-Modelle mit verschiedenen Plänen.
Beispiel-Tiers
| Plan | Requests/Monat | Rate Limit | Preis |
|---|---|---|---|
| Free | 10.000 | 10/s | $0 |
| Starter | 100.000 | 50/s | $29 |
| Pro | 1.000.000 | 200/s | $99 |
| Enterprise | Unlimited | Custom | Custom |
Quota-Überschreitung
Was passiert, wenn die Quota erschöpft ist?
| Strategie | Verhalten | Use Case |
|---|---|---|
| Hard Block | 429, bis nächster Monat | Strikte Kostenkontrolle |
| Soft Block | Warnung, dann Block | User-freundlicher |
| Overage | Weiter, extra Kosten | Pay-as-you-go |
| Throttle | Reduzierte Rate | Graceful Degradation |
HTTP/1.1 429 Too Many Requests
RateLimit-Policy: "monthly";q=10000;w=2592000
RateLimit: "monthly";r=0;t=86400
Retry-After: 86400
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/quota-exceeded",
"title": "Quota Exceeded",
"status": 429,
"error_code": "QUOTA_EXCEEDED",
"detail": "Monthly quota exceeded. Upgrade your plan or wait until next month.",
"upgrade_url": "https://example.com/pricing",
"request_id": "req_def456"
}
Abuse Handling
Manche Clients sind böswillig. Dafür braucht es härtere Maßnahmen.
Abuse-Indikatoren
| Indikator | Beispiel |
|---|---|
| Extrem hohe Rate | 1000x über Limit |
| Pattern | Sequentielle ID-Enumeration |
| Bekannte Bad Actors | IP auf Denylist |
| Credential Stuffing | Viele fehlgeschlagene Logins |
| Scraping | Systematisches Abrufen aller Daten |
Maßnahmen
1. Temporärer Block:
if abuse_detected(request):
block_ip(request.ip, duration="1h")
return Response(status=403, body="Temporarily blocked")
2. CAPTCHA Challenge:
HTTP/1.1 429 Too Many Requests
{
"error_code": "CHALLENGE_REQUIRED",
"challenge_url": "https://api.example.com/challenge/abc123"
}
3. Permanent Block:
# Manuell nach Review
DENYLIST_IPS = load_denylist()
if request.ip in DENYLIST_IPS:
return Response(status=403, body="Access denied")
Denylist und Allowlist
# config/rate_limits.yaml
denylist:
ips:
- 192.168.1.100
- 10.0.0.0/8
user_agents:
- "BadBot/1.0"
- "Scraper*"
allowlist:
ips:
- 203.0.113.50 # Monitoring Service
api_keys:
- "sk_partner_trusted" # Trusted Partner
Monitoring und Alerts
# Prometheus Alerts
groups:
- name: rate_limiting
rules:
- alert: HighRateLimitRejections
expr: sum(rate(http_requests_rejected_total{reason="rate_limit"}[5m])) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "High rate of rejected requests"
- alert: PotentialAbuse
expr: sum(rate(http_requests_rejected_total{reason="rate_limit"}[1m])) by (ip) > 1000
for: 1m
labels:
severity: critical
annotations:
summary: "Potential abuse from "
Implementierung
Redis-basiertes Rate Limiting
import redis
import time
r = redis.Redis()
def is_rate_limited(key: str, limit: int, window: int) -> bool:
"""
Fixed Window Rate Limiter
Args:
key: Identifier (user_id, ip, etc.)
limit: Max requests per window
window: Window size in seconds
"""
current_window = int(time.time() / window)
redis_key = f"ratelimit:{key}:{current_window}"
current = r.incr(redis_key)
if current == 1:
r.expire(redis_key, window)
return current > limit
def get_rate_limit_info(key: str, limit: int, window: int) -> dict:
now = int(time.time())
current_window = int(now / window)
redis_key = f"ratelimit:{key}:{current_window}"
current = int(r.get(redis_key) or 0)
reset_in = (current_window + 1) * window - now
return {
"limit": limit,
"remaining": max(0, limit - current),
"reset": max(0, reset_in)
}
Token Bucket mit Redis
def token_bucket_allow(key: str, rate: float, burst: int) -> bool:
"""
Token Bucket Rate Limiter
Args:
key: Identifier
rate: Tokens per second
burst: Max bucket size
"""
now = time.time()
redis_key = f"tokenbucket:{key}"
# Atomic operation with Lua script
lua_script = """
local tokens = tonumber(redis.call('hget', KEYS[1], 'tokens') or ARGV[2])
local last_time = tonumber(redis.call('hget', KEYS[1], 'last_time') or ARGV[3])
local now = tonumber(ARGV[3])
local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
if rate <= 0 then
return 0
end
-- Add tokens based on time passed
local elapsed = now - last_time
tokens = math.min(burst, tokens + elapsed * rate)
-- Try to consume one token
if tokens >= 1 then
tokens = tokens - 1
redis.call('hset', KEYS[1], 'tokens', tokens, 'last_time', now)
local ttl = math.ceil((burst / rate) * 2)
redis.call('expire', KEYS[1], ttl)
return 1
else
return 0
end
"""
result = r.eval(lua_script, 1, redis_key, rate, burst, now)
return result == 1
Middleware-Beispiel
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
async def rate_limit_middleware(request: Request, call_next):
# Determine key
if request.user:
key = f"user:{request.user.id}"
policy = "user"
limit, window = 100, 60
else:
key = f"ip:{request.client.host}"
policy = "ip"
limit, window = 20, 60
# Check limit
if is_rate_limited(key, limit, window):
info = get_rate_limit_info(key, limit, window)
return JSONResponse(
status_code=429,
content={
"error_code": "RATE_LIMIT_EXCEEDED",
"detail": "Too many requests"
},
headers={
"RateLimit-Policy": f'"{policy}";q={limit};w={window}',
"RateLimit": f'"{policy}";r={info["remaining"]};t={info["reset"]}',
"Retry-After": str(info["reset"])
}
)
# Process request
response = await call_next(request)
# Add headers
info = get_rate_limit_info(key, limit, window)
response.headers["RateLimit-Policy"] = f'"{policy}";q={limit};w={window}'
response.headers["RateLimit"] = f'"{policy}";r={info["remaining"]};t={info["reset"]}'
return response
Regeln & Anti-Patterns
Do
- Rate Limits UND Quotas kombinieren
- Mehrere Dimensionen (User, Tenant, Endpoint)
- Standard-Header (
RateLimit,RateLimit-Policy,Retry-After) - Token Bucket für Burst-Erlaubnis
- Endpoint-spezifische Limits für teure Operationen
- Monitoring und Alerting
- Dokumentierte Limits pro Plan
Don't
- Nur IP-basierte Limits (Shared IPs, IPv6)
- Limits ohne Header (Client weiß nicht, wann er darf)
- Zu aggressive Limits (frustriert legitime User)
- Keine Limits (DoS-Risiko)
- Undokumentierte Limits (schlechte DX)
- Retry-After vergessen (Clients hammern weiter)
- Abuse ohne Monitoring
Artefakt: Rate-Limit-Policy
# Rate-Limit-Policy
## Übersicht
| Dimension | Limit | Burst | Window |
|-------------|--------------|-------|--------|
| Global | 10.000 req/s | - | - |
| Tenant | 1.000 req/s | 5.000 | 1s |
| User | 100 req/s | 500 | 1s |
| IP (unauth) | 20 req/s | 50 | 1s |
## Endpoint-spezifische Limits
| Endpoint | Limit | Burst | Begründung |
|-----------------------------|--------|-------|--------------------|
| `POST /auth/login` | 5/min | 10 | Brute-Force-Schutz |
| `POST /auth/password-reset` | 3/hour | 3 | E-Mail-Spam |
| `POST /search` | 30/min | 50 | Teure Query |
| `POST /export` | 5/hour | 5 | Sehr teuer |
| `GET /reports` | 10/min | 20 | DB-intensiv |
## Quotas
| Plan | Requests/Monat | Rate Limit | Burst |
|------------|----------------|------------|--------|
| Free | 10.000 | 10/s | 50 |
| Starter | 100.000 | 50/s | 250 |
| Pro | 1.000.000 | 200/s | 1.000 |
| Enterprise | Custom | Custom | Custom |
## Response Headers
Alle Responses enthalten:
```http
RateLimit-Policy: "user";q=100;w=60
RateLimit: "user";r=42;t=30
```
Bei zusätzlicher Quota-Policy (Reihenfolge beachten):
```http
RateLimit-Policy: "user";q=100;w=60, "monthly";q=10000;w=2592000
RateLimit: "monthly";r=7523;t=86400
```
## 429 Response
```json
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"error_code": "RATE_LIMIT_EXCEEDED",
"detail": "Rate limit exceeded. Please wait 30 seconds.",
"request_id": "req_abc123"
}
```
Header: `Retry-After: 30`
## Quota-Überschreitung (Beispiel-Response)
```json
{
"type": "https://api.example.com/errors/quota-exceeded",
"title": "Quota Exceeded",
"status": 429,
"error_code": "QUOTA_EXCEEDED",
"detail": "Monthly quota exceeded. Upgrade your plan.",
"upgrade_url": "https://example.com/pricing",
"request_id": "req_def456"
}
```
## Abuse Handling (Beispiele)
### Automatische Blocks
| Trigger | Aktion | Dauer |
|-----------------------------|-----------------|---------------|
| 10x Rate Limit in 1 min | Temp Block | 5 min |
| 100x Rate Limit in 5 min | Temp Block | 1 hour |
| Credential Stuffing Pattern | CAPTCHA | Bis gelöst |
| Known Bad Actor | Permanent Block | Manual Review |
### Allowlist
Trusted Partners und interne Services werden nicht limitiert:
- Monitoring Service (IP: x.x.x.x)
- Partner API Key: `sk_partner_*`
## Client-Empfehlungen
1. **Rate-Limit-Header beachten**: `RateLimit` und `Retry-After` prüfen
2. **Exponential Backoff**: Bei 429 warten, dann Retry
3. **Retry-After respektieren**: Nicht früher erneut versuchen
4. **Batch-Requests nutzen**: Mehrere Items pro Request
5. **Caching**: Responses cachen, weniger Requests
## Monitoring
Alerts bei:
- Rate Limit Rejections > 100/min (Warning)
- Rate Limit Rejections > 1000/min (Critical)
- Einzelne IP > 1000 Rejections/min (Abuse)
- Quota Usage > 80% (Warning)
- Quota Usage > 95% (Critical)
Checkliste
Bevor du zum nächsten Artikel gehst, prüfe:
- [ ] Rate Limits sind pro User/Tenant definiert
- [ ] Quotas sind pro Plan definiert (wenn relevant)
- [ ] Endpoint-spezifische Limits für teure Operationen
- [ ] Token Bucket oder Sliding Window implementiert
- [ ]
RateLimitundRateLimit-PolicyHeader in allen Responses - [ ]
Retry-AfterHeader bei 429 - [ ] Error Response bei Limit-Überschreitung ist informativ
- [ ] Abuse-Handling ist implementiert
- [ ] Monitoring und Alerts sind konfiguriert
- [ ] Rate-Limit-Policy ist dokumentiert
- [ ] Client-Dokumentation erklärt Limits
Wie es weitergeht
Im nächsten Teil geht es um Resilience und Idempotency: Wie du deine API robust gegen Fehler machst und sicherstellst, dass wiederholte Requests keine Probleme verursachen.
Alle Teile der Serie: Serie: API-Design