Jede öffentliche API braucht einen Rate Limiter, und die meisten Teams merken es auf dieselbe Weise: Ein Client mit einer Retry-Schleife oder ein einzelner Scraper legt den Service für alle anderen lahm. Ein Limiter begrenzt, wie viel ein einzelner Key, Nutzer oder eine IP anfragen darf. Ein einzelner Störenfried kostet dich dann ein 429 statt eines Ausfalls.
Die Designfragen sind immer dieselben: welcher Algorithmus, wo der Limiter sitzt, wo die Zähler liegen und was passiert, wenn der Zählerspeicher ausfällt. Dieser Beitrag beantwortet jede davon und endet mit einem Architekturdiagramm eines verteilten Limiters vor einer API.
Was der Limiter leisten muss
- Bei jeder Anfrage in wenigen Millisekunden entscheiden, ob sie erlaubt ist.
- Unterschiedliche Regeln auf unterschiedliche Aufrufer anwenden: pro API-Key, pro Nutzer, pro IP, pro Route.
- Sich gleich verhalten, egal welche Serverinstanz die Anfrage bearbeitet.
- Clients klar sagen, wann sie begrenzt werden und wann sie es erneut versuchen sollen.
- Weiterlaufen oder vorhersehbar ausfallen, wenn der eigene Speicher ein Problem hat.
Der dritte Punkt macht daraus ein Problem verteilter Systeme. Ein Limit von 100 Anfragen pro Minute bedeutet nichts, wenn jede von zehn Gateway-Instanzen ihre eigenen 100 zählt.
Fünf Algorithmen
| Algorithmus | Wie er zählt | Stärke | Schwäche |
|---|---|---|---|
| Token Bucket | Ein Eimer füllt sich mit fester Rate; jede Anfrage nimmt ein Token | Erlaubt kurze Bursts bis zur Eimergröße | Zwei Parameter zum Einstellen |
| Leaky Bucket | Anfragen fließen mit fester Rate aus einer Warteschlange ab | Gleichmäßige Ausgaberate | Bursts warten in der Warteschlange und erhöhen die Latenz |
| Fixed-Window-Zähler | Ein Zähler pro Key und Minute | Am günstigsten: ein Inkrement | Rund um eine Fenstergrenze kann das doppelte Limit durchgehen |
| Sliding-Window-Log | Speichert einen Zeitstempel pro Anfrage | Exakt | Der Speicher wächst mit dem Traffic |
| Sliding-Window-Zähler | Gewichtet den Zähler des vorherigen Fensters nach Überlappung | Nahezu exakt, zwei Zähler pro Key | Eine Näherung |
Das Fixed-Window-Problem ist leicht zu sehen: Ein Client schickt um 12:00:59 Uhr 100 Anfragen und um 12:01:00 Uhr weitere 100, und beide Fenster akzeptieren sie. Das sind 200 Anfragen in zwei Sekunden bei einem Limit von 100 pro Minute.
Token Bucket ist der übliche Standard für APIs, weil Bursts normales Client-Verhalten sind. Ein Limit wie „100 Anfragen pro Minute, Burst 20“ lässt sich direkt auf eine Nachfüllrate und eine Eimergröße abbilden. Stripe hat in seinem Beitrag über das Skalieren einer API mit Rate Limitern beschrieben, wie es Token-Bucket-Limiter auf Redis betreibt, ergänzt um einen separaten Limiter für gleichzeitige Anfragen.
Sliding-Window-Zähler sind der günstige Weg, das Grenzproblem zu beseitigen, wenn du ein hartes Limit pro Fenster willst.
Wo der Limiter sitzt
Du kannst Limits an drei Stellen durchsetzen, und viele Systeme nutzen mehr als eine:
- An der Edge (CDN oder WAF). Grobe Limits pro IP, um Fluten abzufangen, bevor sie dich erreichen.
- Am API-Gateway. Limits pro Key und pro Route, die wissen müssen, wer der Aufrufer ist. Hier leben die meisten Produktlimits.
- In einem Service. Limits, die von Geschäftsdaten abhängen, etwa „zwei gleichzeitige Exporte pro Konto“.
Ein Gateway kann lokal zählen (jede Instanz hat eigene Zähler) oder global (alle Instanzen teilen einen Speicher). Lokale Limits kosten nichts und reichen für groben Schutz; teile das Limit durch die Zahl der Instanzen und akzeptiere die Ungenauigkeit. Produktlimits, für die Kunden bezahlen, brauchen einen globalen Speicher. Envoy unterstützt zum Beispiel beides: einen lokalen Rate-Limit-Filter und einen globalen Modus, der einen externen, von Redis gestützten Rate-Limit-Service aufruft.
Zähler in Redis
Redis ist die übliche Wahl für globale Zähler: schnell, hält Zähler im Speicher und lässt Keys selbstständig ablaufen. Die Falle ist das Rennen zwischen dem Lesen eines Zählers und dem Zurückschreiben. Zwei Gateways lesen 99, beide erlauben die Anfrage, beide schreiben 100.
Halte jede Entscheidung atomar. Für ein Fixed Window reicht INCR gefolgt von EXPIRE auf einem neuen Key; die Redis-Dokumentation zu INCR zeigt darauf aufbauende Rate-Limiting-Muster. Für einen Token Bucket lässt du ein kleines Lua-Skript laufen, das den Eimer liest, ihn anhand der vergangenen Zeit auffüllt, ein Token nimmt und alles in einem Schritt zurückschreibt. Redis führt ein Skript aus, ohne andere Befehle dazwischenzuschieben, also sehen gleichzeitige Anfragen einen konsistenten Eimer.
-- KEYS[1] = bucket key, ARGV = rate per second, capacity, now (ms), cost
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local rate, cap, now, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4])
local tokens = tonumber(b[1]) or cap
local ts = tonumber(b[2]) or now
tokens = math.min(cap, tokens + (now - ts) / 1000 * rate)
local allowed = tokens >= cost
if allowed then tokens = tokens - cost end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(cap / rate * 1000))
return allowed and 1 or 0
Gib die Zeit vom Aufrufer mit oder nutze den Redis-Befehl TIME; die Uhren vieler Gateways zu mischen ist eine häufige Quelle seltsamer Ergebnisse. Wenn dasselbe Redis auch als Cache dient, achte auf die Eviction-Policy: Ein verdrängter Eimer ist ein zurückgesetztes Limit. Der Beitrag zu Caching-Strategien behandelt die Eviction-Einstellungen.
Dem Client Bescheid geben
Beantworte eine begrenzte Anfrage mit 429 Too Many Requests, definiert in RFC 6585, und füge einen Retry-After-Header mit der Wartezeit in Sekunden hinzu. Gute Clients ziehen sich zurück; die anderen bekommen wenigstens ein klares Signal. Ein IETF-Entwurf, RateLimit header fields for HTTP, schlägt Standard-Header vor, die Clients ihr Kontingent und ihr Restbudget nennen, bevor sie das Limit erreichen; zum Zeitpunkt dieses Beitrags (Oktober 2026) ist er noch ein Entwurf, behandle Header-Namen daraus also als vorläufig.
Dokumentiere die Limits. Ein Limit, das Clients erst durch eine Sperre entdecken, wird zum Support-Ticket.
Wenn Redis ausfällt
Entscheide das, bevor es passiert:
- Fail open. Erlaube Anfragen, wenn der Limiter nicht entscheiden kann. Die API bleibt erreichbar, und so lange fehlt dir der Schutz. Das ist die übliche Wahl für Produktlimits.
- Fail closed. Lehne Anfragen ab. Richtig für teure oder missbrauchsanfällige Operationen wie Login-Versuche oder SMS-Versand.
- Auf lokale Limits zurückfallen. Jedes Gateway setzt ein grobes lokales Limit durch, bis der Speicher zurück ist.
Was immer du wählst, alarmiere darauf. Ein Limiter, der eine Woche lang still auf fail open steht, ist kein Limiter.
Das Architekturdiagramm
Diagramminhalt
- API clients
- Load balancer
- API gateway
- Rate limit service
- Redis: counters
- Under the limit?
- Orders API
- 429 + Retry-After
- Metrics
- key + route
- atomic Lua script
- yes
- no
- allowed vs rejected
So liest du es:
- Von links nach rechts verläuft der Anfragepfad: Clients, Load Balancer, API-Gateway und Rate-Limit-Service.
- Der Rate-Limit-Service besitzt die Regeln und trifft pro Anfrage eine Entscheidung. Er hält die Zähler in Redis und aktualisiert sie mit einem atomaren Lua-Skript.
- Der Entscheidungsknoten schickt erlaubte Anfragen an die Orders API. Der Rest bekommt ein 429 mit Retry-After und erreicht das Backend nie.
- Metriken sind gestrichelt, weil sie asynchron gemeldet werden. Abgelehnte Anfragen pro Regel sind das Erste, was du prüfst, wenn ein Kunde sagt, er werde blockiert.
- Der Markdown-Block listet die Regeln in der Reihenfolge ihrer Prüfung, einschließlich des Fehlermodus.
Was ins Diagramm gehört
- Die Regeln und ihre Keys. „100/min“ bedeutet nichts ohne „pro API-Key“ oder „pro IP“.
- Lokale oder globale Zählung. Der Unterschied entscheidet, ob Limits über Instanzen hinweg halten.
- Der Fehlermodus. Fail open oder fail closed gehört ins Diagramm, nicht nur in den Code.
- Der Ablehnungspfad. Zeig, wo ein 429 entsteht, damit niemand annimmt, das Backend sehe diese Anfragen.
Für das Gateway selbst behandelt das Architekturdiagramm für API-Gateways Routing, Authentifizierung und die anderen Filter, die neben dem Limiter laufen. Das Beispiel für ein System-Design-Diagramm zeigt, wie ein Limiter in ein größeres Design passt.
In Archboard nachbauen
- Melde dich mit Google oder GitHub an und leg ein Diagramm an.
- Füge aus dem C4-Modell-Pack eine Person für die Clients hinzu, dann aus dem Cloud-Pack einen Cloud-Lastverteiler und ein API-Gateway.
- Füge aus dem C4-Modell-Pack zwei Container-Nodes hinzu: den Rate-Limit-Service und die Orders API. Setz unter den Rate-Limit-Service Redis aus dem Daten-Pack.
- Füge für den Zweig Erlauben oder Ablehnen eine Entscheidung und ein Start / Ende aus dem Flussdiagramm-Pack hinzu und Metriken aus dem DevOps-Pack.
- Beschrifte die Kanten, zeichne die Metrik-Kante gestrichelt und füge einen Markdown-Block aus dem Basis-Pack mit den Regeln hinzu.
Das Beispiel nutzt 10 Diagramm-Nodes und bleibt damit im Free-Plan.
FAQ
Welchen Rate-Limiting-Algorithmus sollte ich nutzen? Token Bucket für die meisten APIs, weil er die kurzen Bursts erlaubt, die echte Clients erzeugen. Nimm einen Sliding-Window-Zähler, wenn du ein striktes Limit pro Fenster brauchst, ohne die Kosten eines vollständigen Anfrage-Logs.
Sollte der Rate Limiter ein eigener Service sein? Nicht unbedingt. Ein Gateway-Filter mit einem gemeinsamen Redis reicht für viele Systeme. Ein eigener Service hilft, wenn viele Gateways und Services dieselben Regeln teilen.
Welchen Statuscode sollte eine begrenzte Anfrage bekommen? 429 Too Many Requests, mit einem Retry-After-Header. Nimm kein 503; das sagt Clients, dein Service sei ausgefallen, statt dass sie zu viel angefragt haben.
Wie begrenze ich pro Nutzer, wenn Anfragen über viele Server kommen? Halte die Zähler in einem gemeinsamen Speicher wie Redis, mit Nutzer oder API-Key als Schlüssel, und aktualisiere sie atomar. Lokale Zähler pro Server taugen nur als grobe Obergrenze.
Was passiert, wenn der Rate Limiter selbst ausfällt? Entscheide es vorher. Fail open für normale Produktlimits, fail closed für teure oder missbrauchsanfällige Operationen, und in beiden Fällen ein Alarm.