TS
Thomas Schmitz

Freiberuflicher IT-Berater, Softwareentwickler & DevOps. Praxisnahe Artikel zu API-Design, Architektur und Cloud-Workflows.

Serie: API-Design · Teil 13 von 22

API-Design Teil 13: Rate Limiting & Quotas

Wie du Limits setzt, kommunizierst und durchsetzt – ohne legitime Clients zu blockieren.

Praxisnah Checkliste

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:

  1. Rate Limit oder Quota? Kurzfristig (pro Sekunde) oder langfristig (pro Monat)?
  2. Welche Dimension? Pro User? Pro IP? Pro Tenant? Pro API Key?
  3. Welche Endpoints brauchen eigene Limits? Login? Search? Export?
  4. Wie kommuniziert ihr Limits? Header? Dokumentation?
  5. 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.

Outflow: 10 Requests/Sekunde

Incoming

Queue (100 Plätze)

Processing (konstant)

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
  • [ ] RateLimit und RateLimit-Policy Header in allen Responses
  • [ ] Retry-After Header 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

Mehr Beiträge aus dem Blog.