Back to Blog
January 22, 20261,000 views

Ratenbegrenzung, Circuit Breaker und Wiederholungsstürme: Aufbau robuster APIs

Ihre API verarbeitet problemlos 1.000 Anfragen pro Sekunde. Dann fällt ein nachgelagerter Dienst aus, die Clients starten erneute Versuche, und plötzlich werden Sie mit 50.000 Anfragen pro Sekunde überschwemmt. So verhindern Sie Kettenausfälle.

Die Flut von Wiederholungsversuchen, die die Produktion lahmlegte

Folgendes ist in einem realen System passiert: Eine Datenbankreplik fiel für zwei Minuten aus. Die API gab daraufhin 500-Fehler zurück. Jeder Client verfügte über eine Wiederholungslogik: drei Wiederholungsversuche ohne Backoff. 1.000 Clients × 3 Wiederholungsversuche = 3.000 Anfragen pro Sekunde, die auf einen ohnehin schon überlasteten Server trafen. Die Primärdatenbank konnte diesen Ansturm nicht bewältigen. Nun war alles ausgefallen.

Dies wird als „Retry Storm“ bezeichnet und ist die häufigste Ursache für Kettenausfälle in verteilten Systemen.

Lösung 1: Exponentieller Backoff mit Jitter

Versuchen Sie es niemals sofort erneut. Verwenden Sie einen exponentiellen Backoff (bei jedem erneuten Versuch länger warten) mit zufälligem Jitter (damit nicht alle Clients gleichzeitig einen erneuten Versuch unternehmen):

import random
import time
import httpx

def fetch_with_retry(url, max_retries=3):
    for attempt in range(max_retries):
        try:
            response = httpx.get(url, timeout=5.0)
            response.raise_for_status()
            return response.json()
        except (httpx.HTTPStatusError, httpx.TimeoutException) as e:
            if attempt == max_retries - 1:
                raise
            
            # Exponential backoff: 1s, 2s, 4s
            base_delay = 2 ** attempt
            # Jitter: randomize to prevent thundering herd
            jitter = random.uniform(0, base_delay)
            delay = base_delay + jitter
            
            print(f"Retry {attempt + 1}/{max_retries} in {delay:.1f}s")
            time.sleep(delay)

Lösung 2: Muster für den Leistungsschalter

Ein Circuit Breaker protokolliert Fehler. Nach Überschreiten eines Schwellenwerts (z. B. 5 Fehler innerhalb von 30 Sekunden) „öffnet“ er sich und gibt sofort Fehler zurück, ohne den nachgeschalteten Dienst aufzurufen. Nach einer Abkühlphase lässt er eine Anfrage durch, um zu prüfen, ob sich der Dienst wieder erholt hat.

import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = 'closed'       # normal operation
    OPEN = 'open'           # failing, reject all requests
    HALF_OPEN = 'half_open' # testing recovery

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = None
    
    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise CircuitBreakerOpen("Service unavailable")
        
        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise
    
    def _on_success(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED
    
    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN


# Usage
payment_breaker = CircuitBreaker(failure_threshold=5)

try:
    result = payment_breaker.call(stripe.PaymentIntent.create, amount=1000)
except CircuitBreakerOpen:
    return Response(
        {'error': 'Payment service temporarily unavailable'},
        status=503
    )

Lösung 3: Serverseitige Ratenbegrenzung

Schützen Sie Ihre API sowohl vor legitimer Überbeanspruchung als auch vor böswilligem Missbrauch. Nutzen Sie einen auf Redis basierenden Rate-Limiter mit gleitendem Zeitfenster:

# Django middleware: Redis-backed sliding window rate limiter
import time
import redis

class RateLimitMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.redis = redis.Redis(host='redis', port=6379)
    
    def __call__(self, request):
        client_ip = request.META.get('HTTP_X_FORWARDED_FOR', 
                                      request.META['REMOTE_ADDR'])
        key = f'rate_limit:{client_ip}'
        
        # Sliding window: 100 requests per 60 seconds
        now = time.time()
        pipe = self.redis.pipeline()
        pipe.zremrangebyscore(key, 0, now - 60)  # remove old entries
        pipe.zadd(key, {str(now): now})           # add current request
        pipe.zcard(key)                           # count requests
        pipe.expire(key, 60)                      # auto-cleanup
        _, _, request_count, _ = pipe.execute()
        
        if request_count > 100:
            return JsonResponse(
                {'error': 'Rate limit exceeded. Try again in 60s.'},
                status=429,
                headers={'Retry-After': '60'}
            )
        
        return self.get_response(request)

Die Checkliste zur Resilienz

Client-seitig: Exponentieller Backoff + Jitter bei allen Wiederholungsversuchen. Zeitlimits für Anfragen festlegen (niemals unbegrenzt warten). Circuit Breaker für jede externe Abhängigkeit implementieren.

Serverseitig: Ratenbegrenzung pro IP-Adresse und pro API-Schlüssel. Graceful Degradation (Rückgabe von zwischengespeicherten Daten bei langsamer Datenbank). Endpunkte für Zustandsprüfungen für Lastenausgleichsserver. Bulkhead-Muster (Trennung kritischer Dienste von nicht kritischen Diensten).

Infrastruktur: Automatische Skalierung basierend auf der Länge der Anforderungswarteschlange, nicht nur auf der CPU-Auslastung. Separate Lesereplikate für Endpunkte mit hohem Leseaufkommen. CDN für statische Inhalte und zwischenspeicherbare API-Antworten.

Alles geht ständig kaputt. Die Frage ist nicht, ob deine Abhängigkeiten ausfallen, sondern ob dein System damit problemlos umgeht, wenn es doch passiert.

— Werner Vogels, Technischer Leiter bei Amazon