Ein Client, der in einer Retry-Schleife ohne Backoff feststeckt, kann Ihrer API in einer Minute mehr Last aufbĂŒrden als Ihre ĂŒbrigen zehntausend Clients zusammen. Von Ihrer Seite aus sieht das nicht nach einem Angriff aus. Es sieht nach einem Bug bei ihnen aus, und es wird zum Incident bei Ihnen. Ein Rate Limiter bremst diesen Client, bevor es so weit kommt.
Was entscheidet ein Rate Limiter eigentlich?
Bei jedem Request stellt der Limiter nur eine Frage: Hat dieser Key im aktuellen Zeitfenster zu viele Requests gemacht? Drei Entscheidungen machen daraus etwas, das Sie implementieren können.
Der Key ist das, wogegen Sie zÀhlen: eine IP-Adresse, ein API-Key, die ID eines authentifizierten Nutzers. WÀhlen Sie den falschen, blockieren Sie entweder unschuldige Nutzer, die sich eine IdentitÀt teilen, oder lassen den eigentlichen Missbraucher sich unter ihnen verstecken.
Das Zeitfenster und das Limit ergeben nur zusammen einen Sinn. â100 Requests" ist kein Limit; 100 pro Sekunde und 100 pro Tag sind zwei sehr unterschiedliche APIs.
Die Aktion ist das, was passiert, wenn der Key ĂŒber dem Limit liegt. Die meisten APIs lehnen mit 429 Too Many Requests ab. Ein Hintergrundjob oder ein Webhook-Versender stellt die Arbeit eher in eine Queue und arbeitet sie spĂ€ter ab â genau die Aufgabe einer Message Queue (siehe Was ist eine Message Queue und wie wird sie verwendet).
Welcher Algorithmus, welcher Store, welche Header: Das alles ist Implementierung rund um diese eine PrĂŒfung.
Vier Rate-Limiting-Algorithmen und ihre jeweilige Schwachstelle
Fixed Window Counter. Erhöhen Sie einen ZĂ€hler fĂŒr den Key und setzen Sie ihn zurĂŒck, wenn die Uhr eine Grenze ĂŒberschreitet, etwa jede Minute zur vollen Minute. Kostet einen Integer pro Key. Der Haken: Ein Client, der 100 Requests um 0:59 sendet und weitere 100 um 1:00, hat 200 Requests in zwei Sekunden abgeschickt, ohne â100 pro Minute" in einem der beiden Fenster zu verletzen. An der Grenze lĂ€sst ein Fixed Window bis zum Doppelten der angegebenen Rate durch.
Sliding Window Log. Speichern Sie den Timestamp jedes Requests fĂŒr den Key (ein Redis Sorted Set passt gut), und zĂ€hlen Sie bei jedem neuen Request, wie viele in die letzten 60 Sekunden fallen. Das ist exakt, der Grenz-Trick funktioniert also nicht. Der Preis ist Speicher, der mit dem Traffic wĂ€chst: eine Million Timestamps pro Key zu zĂ€hlen ist nicht dasselbe wie einen Integer zu lesen â derselbe Unterschied wie zwischen dem Durchsuchen einer Liste und dem Lesen eines Index, dem Big-O-Notation ErklĂ€rt mit Code-Beispielen einen Namen gibt.
Sliding Window Counter. Das ist der, den die meisten APIs verwenden sollten. Halten Sie zwei Fixed-Window-ZĂ€hler, den aktuellen und den vorherigen, und schĂ€tzen Sie den gleitenden Wert als aktueller_zaehler + vorheriger_zaehler * (Ăberlappungsanteil). Sind Sie 40% in die aktuelle Minute, zĂ€hlen Sie alle Requests im aktuellen Fenster plus 60% des vorherigen. Zwei Integer pro Key, kein Grenz-Exploit, und nah genug am Exakten fĂŒr reale Limits.
Token Bucket. Ein Bucket fasst bis zu capacity Tokens und fĂŒllt sich mit einer festen rate pro Sekunde auf. Jeder Request verbraucht ein Token; kein Token, kein Request. Ein Client kann einen Burst von bis zu capacity Requests auf einmal machen und wird dann auf die Refill-Rate gedrosselt. Das entspricht dem, wie Browser und Apps APIs tatsĂ€chlich aufrufen: Ein Seitenaufruf feuert acht Requests gleichzeitig ab, dann eine Weile nichts. Eine feste Sekundengrenze wĂŒrde die HĂ€lfte dieses Seitenaufrufs ablehnen.
Leaky Bucket. Derselbe Bucket, das gegenteilige Ziel. Eingehende Requests landen in einer Queue und verlassen sie mit konstanter Rate, sodass aus Bursts ein gleichmĂ€Ăiger Strom wird. Der Token Bucket entscheidet, wie viel Burst einem Client erlaubt ist. Der Leaky Bucket entscheidet, wie konstant die ausgehende Rate ist. Greifen Sie darauf zurĂŒck, wenn das, was nachgeschaltet ist (eine Drittanbieter-API mit eigener Quota oder ein Worker-Pool fester GröĂe), eine konstante Zufuhr braucht.
Einen Rate Limiter mit Redis bauen
Der ZĂ€hler muss an einem Ort leben, den jede Instanz Ihrer API sehen kann. Ein prozessinterner ZĂ€hler funktioniert, solange Sie nur eine Instanz betreiben; sobald eine zweite dazukommt, setzt jede das Limit nur gegen die HĂ€lfte des Traffics durch. Redis ist der ĂŒbliche Store (siehe Wie Redis-Caching funktioniert und wie Sie es nutzen, falls Sie noch keinen eingerichtet haben), und der Grund dafĂŒr sind seine atomaren Befehle mehr als seine Geschwindigkeit.
Ein Fixed Window Counter in Python mit INCR und EXPIRE:
| |
INCR ist auf einer einzelnen Redis-Instanz atomar, sodass zwei Requests, die in derselben Mikrosekunde eintreffen, trotzdem zwei unterschiedliche ZĂ€hlerwerte zurĂŒckbekommen. Der Bug, den die meisten selbstgebauten Limiter mit sich herumtragen, ist ein GET gefolgt von einem SET. Zwei gleichzeitige Requests lesen denselben ZĂ€hlerstand, beide entscheiden, dass sie unter dem Limit liegen, und beide kommen durch. Eine LĂŒcke bleibt in der Version oben: Stirbt Ihr Prozess zwischen INCR und EXPIRE, lĂ€uft der Key nie ab, und der Client bleibt blockiert. Beide Aufrufe in ein Lua-Skript zu verschieben, wie beim Token Bucket unten, schlieĂt diese LĂŒcke.
Der Token Bucket braucht drei Schritte: den Refill berechnen, ein Token nehmen, den Zustand zurĂŒckschreiben. Kein einzelner Redis-Befehl macht alle drei, also fĂŒhren Sie sie als Lua-Skript ĂŒber EVAL aus, das Redis als eine atomare Einheit ausfĂŒhrt:
| |
Aufgerufen wird es mit redis.eval(script, 1, f"bucket:{key}", capacity, refill_rate, time.time()). Das Skript fĂŒllt den Bucket fĂŒr die vergangene Zeit auf, nimmt im selben Durchgang ein Token und setzt ein TTL, das lang genug fĂŒr einen vollstĂ€ndigen Refill ist â so belegt ein Client, der aufhört, die API aufzurufen, nicht fĂŒr immer Platz in Redis. Kein anderer Request kann den Bucket lesen oder Ă€ndern, wĂ€hrend das Skript lĂ€uft.
Welche Header Sie mit einer 429-Antwort senden sollten
Eine abgelehnte Anfrage sollte dem Client sagen, wann er es erneut versuchen soll.
Verwenden Sie 429 Too Many Requests, nicht 403. Der Client ist nicht verboten, er ist zu frĂŒh.
FĂŒgen Sie Retry-After: 30 hinzu, in Sekunden oder als HTTP-Datum. Viele Client-Bibliotheken und HTTP-SDKs lesen ihn, um ihren Retry zu planen, und die, die es nicht tun, geben einem Menschen zumindest einen Anhaltspunkt.
Senden Sie X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset bei jeder Antwort, nicht nur bei abgelehnten. Sie sagen dem Client die Obergrenze, das verbleibende Kontingent und wann das Fenster zurĂŒckgesetzt wird. Das ist eine De-facto-Konvention (die API von GitHub nutzt sie) und kein Standard, aber es ist das, worauf Client-Entwickler achten. Ein gut funktionierender Client beobachtet, wie Remaining sinkt, und drosselt sich, bevor er je einen 429 sieht.
Eine Standardversion existiert als Entwurf: die Felder RateLimit und RateLimit-Policy aus dem draft-ietf-httpapi-ratelimit-headers der IETF. Das ist seit mehreren Jahren ein Internet-Draft und noch kein RFC. Verschicken Sie die X-RateLimit-*-Header jetzt, und fĂŒgen Sie die Standardfelder hinzu, wenn der Entwurf als RFC veröffentlicht wird.
HĂ€ufige Rate-Limiting-Fehler
Nur nach IP limitieren. Ein Firmennetz, das NAT eines Mobilfunkanbieters oder ein CDN-Edge können Tausende echter Nutzer hinter einer einzigen Adresse verstecken. Verwenden Sie als Key den API-Key oder die ID des authentifizierten Nutzers, wo immer Sie einen haben, und greifen Sie nur fĂŒr anonymen Traffic auf die IP zurĂŒck. JWT vs Session: Welche Authentifizierung WĂ€hlen erklĂ€rt, wie diese IdentitĂ€t zustande kommt.
Ein In-Memory-ZĂ€hler hinter einem Load Balancer. Jede Instanz zĂ€hlt fĂŒr sich, sodass aus einem Limit von 100 am Ende 100 Ă die Anzahl der Instanzen wird. Sobald Sie horizontal skalieren, muss der ZĂ€hler in Redis leben, nicht in einem prozesslokalen Dict.
Ein Limit fĂŒr die gesamte API. Eine Suche und ein Massenexport kosten nicht dasselbe zu bedienen. Setzen Sie Limits pro Route, oder zumindest pro Kostenklasse.
Null Toleranz fĂŒr Bursts. Ein Webhook-Retry, ein Massenimport, eine mobile App, die sich nach einem Tunnel neu verbindet: FĂŒr eine feste Sekundengrenze sieht das alles nach Missbrauch aus. Der Token Bucket existiert genau fĂŒr diesen Traffic.
Welchen Rate Limiter sollten Sie zuerst bauen?
FĂŒr eine typische leseintensive API bauen Sie einen Sliding Window Counter mit dem API-Key als Key, gespeichert in Redis, mit X-RateLimit-*-Headern bei jeder Antwort. Das vermeidet den Grenz-Burst des Fixed Windows und die Speicherkosten eines vollstĂ€ndigen Request-Logs. FĂŒgen Sie einen Token Bucket nur auf den Routen hinzu, bei denen Clients legitim Batches senden (Webhook-Zustellung, Massen-Uploads), und setzen Sie dessen Capacity nach der GröĂe des gröĂten erwarteten Batches, nicht nach der Rate im Dauerbetrieb.