Fixed Window, Sliding Window und Token Bucket richtig einsetzen
Unlimitierte APIs sind ein offenes Tor für Missbrauch, Scraping und versehentliche Overload-Situationen durch fehlerhafte Clients. Dieser Artikel zeigt, wie Fixed Window, Sliding Window und Token Bucket Algorithmen funktionieren, wie Rate Limits pro IP-Adresse oder API-Key sinnvoll konfiguriert werden und wie sich Gateway-Layer und Anwendungslogik in Magento-Umgebungen mit Redis kombinieren lassen, um Systeme stabil und fair zu halten.
Inhaltsverzeichnis
- 1. Warum API-Rate-Limiting unverzichtbar ist
- 2. Rate-Limiting-Algorithmen im Überblick: Fixed Window, Sliding Window, Token Bucket
- 3. Burst-Traffic vs. Missbrauch: legitime Lastspitzen erkennen
- 4. Per-IP vs. Per-API-Key: die richtige Limitierungsstrategie
- 5. Rate-Limit-Header: X-RateLimit-* für kooperative Clients
- 6. Gateway/CDN-Layer vs. Application-Layer
- 7. Redis-basiertes Rate-Limiting in PHP und Magento
- 8. Monitoring und Alerting für Rate-Limiting-Systeme
- 9. Rate-Limiting-Algorithmen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum API-Rate-Limiting unverzichtbar ist
Jede öffentlich erreichbare REST- oder GraphQL-API ist eine Angriffsfläche für automatisierten Missbrauch: Scraping-Bots, die den kompletten Produktkatalog abgreifen, Credential-Stuffing-Angriffe gegen den Login-Endpunkt oder GraphQL-Queries, die durch verschachtelte Relationen künstlich teuer gemacht werden. Ohne Rate-Limiting kann ein einzelner fehlkonfigurierter Client oder ein gezielter Angreifer den Datenbank-Connection-Pool erschöpfen und damit den Service für alle Nutzer gleichzeitig verlangsamen oder komplett lahmlegen. Das OWASP API Security Top 10 Projekt listet unbegrenzten Ressourcenverbrauch (API4:2023) deshalb als eigenständige, hochkritische Schwachstellenkategorie.
Rate-Limiting ist damit keine Optimierung, sondern eine Grundschutzmaßnahme auf derselben Ebene wie Authentifizierung und Input-Validierung. Der Unterschied zu klassischer Firewall-Logik: Rate-Limiting arbeitet auf Anwendungsebene und kennt den Kontext einer Anfrage, etwa welcher Kunde, welcher API-Key oder welcher Endpunkt betroffen ist. Gerade bei Magento-Shops mit öffentlichen REST- und GraphQL-Endpunkten für Produktsuche, Warenkorb und Checkout ist die Kombination aus hoher Reichweite und teuren Datenbankabfragen ein bevorzugtes Ziel für Missbrauch, der ohne Limits erst beim Ausfall sichtbar wird.
2. Rate-Limiting-Algorithmen im Überblick: Fixed Window, Sliding Window, Token Bucket
Der Fixed-Window-Algorithmus zählt Anfragen in festen Zeitfenstern, etwa 100 Requests pro Minute, und setzt den Zähler bei jedem Fensterwechsel zurück. Er ist einfach zu implementieren und benötigt nur einen Counter pro Client, hat aber ein strukturelles Problem an der Fenstergrenze: Ein Client kann kurz vor Fensterende und kurz danach jeweils das volle Limit ausschöpfen und damit kurzzeitig die doppelte Rate erreichen. Der Sliding-Window-Counter mildert dieses Problem, indem er das aktuelle und das vorherige Fenster gewichtet nach verstrichener Zeit kombiniert - eine gute Näherung an echtes Sliding-Window-Verhalten bei minimalem Speicherbedarf.
Der Token-Bucket-Algorithmus füllt einen virtuellen Eimer kontinuierlich mit einer festen Rate an Tokens, bis zu einer definierten Kapazität. Jede Anfrage verbraucht ein Token, und ist der Eimer leer, wird die Anfrage abgelehnt. Das Besondere: Token Bucket erlaubt kontrollierte Bursts, solange genügend Tokens angesammelt wurden, was ihn ideal für APIs mit natürlich schwankendem Traffic macht. Der Sliding-Log-Algorithmus speichert dagegen den exakten Timestamp jeder einzelnen Anfrage und liefert damit die genaueste Rate-Berechnung, verbraucht aber deutlich mehr Speicher pro Client, da die komplette Anfragehistorie im Fenster vorgehalten werden muss.
<?php
declare(strict_types=1);
namespace Mironsoft\RateLimit\Model;
use Redis;
/**
* Redis-backed token bucket rate limiter.
* Atomic refill and consume via a Lua script to avoid race conditions
* under concurrent requests.
*/
final class TokenBucketLimiter
{
private const LUA_SCRIPT = <<<LUA
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'timestamp')
local tokens = tonumber(bucket[1]) or capacity
local last = tonumber(bucket[2]) or now
local elapsed = math.max(0, now - last)
tokens = math.min(capacity, tokens + elapsed * refill_rate)
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'timestamp', now)
redis.call('EXPIRE', key, 3600)
return {allowed, tokens}
LUA;
public function __construct(private readonly Redis $redis)
{
}
/**
* Try to consume tokens for a client. Returns true when the
* request is allowed within the current bucket capacity.
*
* @param string $clientKey Unique identifier, e.g. api-key or ip hash
* @param int $capacity Maximum burst size (bucket size)
* @param float $refillRate Tokens added per second
*/
public function allow(string $clientKey, int $capacity, float $refillRate): bool
{
$result = $this->redis->eval(
self::LUA_SCRIPT,
["ratelimit:bucket:{$clientKey}", (string) $capacity, (string) $refillRate, (string) microtime(true), '1'],
1
);
return (bool) $result[0];
}
}
3. Burst-Traffic vs. Missbrauch: legitime Lastspitzen erkennen
Nicht jeder Traffic-Peak ist ein Angriff. Ein Newsletter-Versand, eine Flash-Sale-Kampagne oder ein viraler Social-Media-Post können innerhalb weniger Sekunden ein Vielfaches des normalen Traffics erzeugen, ohne dass böswillige Absicht vorliegt. Ein zu striktes Fixed-Window-Limit blockiert in solchen Momenten echte Kunden und verursacht damit genau den Umsatzverlust, den Rate-Limiting eigentlich verhindern soll. Die Lösung liegt in der Unterscheidung zwischen erlaubtem Burst und nachhaltigem Missbrauch: Ein Token-Bucket mit ausreichender Kapazität toleriert kurze Lastspitzen, während die Refill-Rate die langfristige durchschnittliche Nutzung begrenzt.
Echte Missbrauchsmuster unterscheiden sich von legitimen Bursts durch ihre Regelmäßigkeit und ihr Fehlen typischer Nutzer-Signale: identische User-Agent-Strings über tausende Requests, fehlende Referer-Header, exakt getaktete Anfrageintervalle im Millisekundenbereich oder sequenzielle Iteration über Produkt-IDs oder Gutscheincode-Muster. Ein mehrstufiges Limitierungskonzept mit weichen Warnschwellen und harten Sperrschwellen erlaubt es, verdächtiges Verhalten zunächst zu drosseln statt sofort zu blockieren, und erst bei anhaltender Überschreitung härtere Maßnahmen wie temporäre IP-Sperren oder CAPTCHA-Herausforderungen auszulösen.
4. Per-IP vs. Per-API-Key: die richtige Limitierungsstrategie
Rate-Limiting nach IP-Adresse ist die einfachste Strategie und eignet sich für öffentliche, nicht authentifizierte Endpunkte wie Produktsuche oder Login-Formulare. Ihr größter Schwachpunkt: Hinter einer einzigen IP-Adresse können durch NAT, Firmennetzwerke oder mobile Carrier-Grade-NAT tausende echte Nutzer stehen, die durch ein zu striktes IP-Limit kollektiv blockiert werden. Umgekehrt kann ein Angreifer mit einem Botnetz aus tausenden IP-Adressen ein reines IP-Limit trivial umgehen, indem er die Last über viele Quellen verteilt.
Für authentifizierte APIs ist Rate-Limiting nach API-Key oder Kunden-ID präziser, da es echte Nutzer sauber voneinander trennt, unabhängig von der IP-Adresse. Die robusteste Lösung kombiniert beide Ebenen: ein moderates IP-Limit als erste Verteidigungslinie gegen anonyme Massenangriffe, plus ein individuelles, planbasiertes Limit pro API-Key für authentifizierte Nutzung. Magento-Integrationen mit Drittsystemen über die REST- oder GraphQL-API sollten dabei unterschiedliche Kontingente je nach Integration Token erhalten, damit ein fehlerhaftes ERP-System nicht das Limit für den Checkout-Client verbraucht.
5. Rate-Limit-Header: X-RateLimit-* für kooperative Clients
Ein Rate-Limit, das Clients nicht kommuniziert, zwingt Integrationspartner zu Trial-and-Error und produziert unnötige 429-Fehler. Die De-facto-Standard-Header X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset geben bei jeder Antwort, auch bei erfolgreichen Requests, Auskunft über das aktuelle Kontingent, die verbleibenden Anfragen und den Unix-Timestamp der nächsten Zurücksetzung. Gut implementierte API-Clients lesen diese Header aus und drosseln sich selbst proaktiv, bevor das Limit überhaupt erreicht wird, statt blind Anfragen zu senden und auf Fehler zu reagieren.
Bei einer 429-Antwort ist zusätzlich der standardisierte Retry-After-Header entscheidend, da er Clients exakt mitteilt, wie viele Sekunden bis zum nächsten erlaubten Versuch zu warten sind, statt mit fixem oder exponentiellem Backoff zu raten. IETF-Draft-Standards wie RateLimit-Limit, RateLimit-Remaining und RateLimit-Policy ohne das X-Präfix gewinnen zunehmend an Verbreitung, aber die X-RateLimit-Variante bleibt in bestehenden Ökosystemen wie GitHub, Twitter/X und Shopify der pragmatische Industriestandard, an dem sich die meisten Integrationen orientieren.
# Inspect rate limit headers on a live API response
curl -sD - -o /dev/null https://api.mironsoft.de/v1/products \
-H "X-Api-Key: demo-key-123"
# Example response headers returned by the API
HTTP/2 200
content-type: application/json
x-ratelimit-limit: 100
x-ratelimit-remaining: 87
x-ratelimit-reset: 1752312600
# Example response headers when the limit is exceeded
HTTP/2 429
content-type: application/json
x-ratelimit-limit: 100
x-ratelimit-remaining: 0
x-ratelimit-reset: 1752312600
retry-after: 42
6. Gateway/CDN-Layer vs. Application-Layer
Rate-Limiting am Gateway oder CDN, etwa über Cloudflare, Fastly oder einen API-Gateway wie Kong, stoppt Missbrauch, bevor die Anfrage überhaupt den Applikationsserver erreicht. Das schont Datenbank-Connections, PHP-FPM-Worker und Backend-Ressourcen und ist besonders wirksam gegen volumetrische Angriffe und einfache Bot-Netzwerke. Der Nachteil: Edge-Layer kennen selten den vollen Anwendungskontext, etwa ob ein authentifizierter Kunde ein Premium-Kontingent hat oder ob eine bestimmte GraphQL-Query durch verschachtelte Felder besonders teuer ist. Grobkörnige IP- oder Pfad-basierte Limits sind hier die Regel.
Application-Layer-Rate-Limiting, direkt im PHP-Code oder als Magento-Plugin implementiert, kennt dagegen den vollen Kontext: eingeloggten Kunden, API-Key-Plan, Query-Komplexität und geschäftslogik-spezifische Limits wie maximal drei Gutscheincode-Versuche pro Bestellung. Die empfohlene Architektur kombiniert beide Ebenen als Verteidigung in der Tiefe: Der Gateway blockt grobe, volumetrische Angriffe frühzeitig und günstig, während die Anwendungsebene feingranulare, geschäftslogikbewusste Limits durchsetzt. Diese Kombination verhindert, dass Angreifer entweder die teure Applikationsschicht überlasten oder ein zu grobes Edge-Limit legitime Premium-Kunden ausbremst.
// Cloudflare Worker: fixed window rate limit at the edge using KV storage
export default {
async fetch(request, env) {
const clientIp = request.headers.get('CF-Connecting-IP') ?? 'unknown';
const windowSeconds = 60;
const limit = 120;
const windowKey = Math.floor(Date.now() / 1000 / windowSeconds);
const key = `rl:${clientIp}:${windowKey}`;
const current = parseInt((await env.RATE_LIMIT_KV.get(key)) ?? '0', 10);
if (current >= limit) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'Retry-After': String(windowSeconds),
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': '0',
},
});
}
// Increment counter with TTL slightly longer than the window
await env.RATE_LIMIT_KV.put(key, String(current + 1), { expirationTtl: windowSeconds + 5 });
const response = await fetch(request);
response.headers.set('X-RateLimit-Limit', String(limit));
response.headers.set('X-RateLimit-Remaining', String(Math.max(0, limit - current - 1)));
return response;
},
};
7. Redis-basiertes Rate-Limiting in PHP und Magento
Magento bringt kein natives, produktionsreifes API-Rate-Limiting für REST- oder GraphQL-Endpunkte mit, weshalb sich eine eigene Plugin-Lösung anbietet, die vor dem eigentlichen Controller ansetzt. Ein around-Plugin auf Magento\Webapi\Controller\Rest oder eine Preference für den GraphQL-Dispatcher prüft das Kontingent, bevor teure Resolver oder Repository-Aufrufe überhaupt ausgeführt werden. Redis eignet sich als zentraler Zähler-Speicher besonders gut, weil Magento in produktiven Setups ohnehin für Session- und Cache-Storage eine Redis-Instanz betreibt und die Latenz für einen Zähler-Zugriff im Sub-Millisekunden-Bereich liegt.
Wichtig bei der Implementierung: Der Zähler-Increment und die Limit-Prüfung müssen atomar erfolgen, sonst entstehen bei parallelen Requests Race-Conditions, die das Limit unterlaufen. Lua-Scripts, die per EVAL an Redis übergeben werden, garantieren diese Atomarität, da Redis Scripts als Ganzes ausführt, ohne dass andere Clients dazwischenfunken. Für Magento-Shops mit mehreren Webserver-Instanzen hinter einem Load Balancer ist ein zentraler Redis-Zähler zudem die einzige zuverlässige Methode, da lokale In-Memory-Zähler pro Server das Gesamtlimit unbemerkt vervielfachen würden.
<?php
declare(strict_types=1);
namespace Mironsoft\RateLimit\Plugin;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Webapi\Rest\Request;
use Magento\Webapi\Controller\Rest\InputParamsResolver;
use Redis;
/**
* Enforces a Redis-backed sliding window limit on REST API requests
* before the resolved controller action is dispatched.
*/
final class SlidingWindowRestLimiterPlugin
{
private const WINDOW_SECONDS = 60;
private const MAX_REQUESTS = 120;
public function __construct(
private readonly Redis $redis,
private readonly Request $request
) {
}
/**
* Rejects the request with HTTP 429 once the client exceeds
* the configured sliding window quota.
*
* @throws LocalizedException
*/
public function beforeResolve(InputParamsResolver $subject): void
{
$apiKey = $this->request->getHeader('X-Api-Key') ?: $this->request->getClientIp();
$key = "ratelimit:sliding:{$apiKey}";
$now = microtime(true);
$windowStart = $now - self::WINDOW_SECONDS;
// Drop timestamps outside the current window, then count and record
$this->redis->zRemRangeByScore($key, '-inf', (string) $windowStart);
$count = $this->redis->zCard($key);
if ($count >= self::MAX_REQUESTS) {
throw new LocalizedException(__('Rate limit exceeded, please retry later.'), null, 429);
}
$this->redis->zAdd($key, $now, (string) $now);
$this->redis->expire($key, self::WINDOW_SECONDS);
}
}
8. Monitoring und Alerting für Rate-Limiting-Systeme
Rate-Limiting ohne Monitoring ist blind: Ohne Sichtbarkeit auf abgelehnte Requests, betroffene Endpunkte und die Verteilung über Client-IDs bleibt unklar, ob Limits zu strikt, zu lax oder genau richtig konfiguriert sind. Zentrale Metriken sind die 429-Rate pro Endpunkt, die Top-N-Clients nach Ablehnungsrate und die Latenz der Rate-Limit-Prüfung selbst, da ein überlasteter Redis-Cluster den Rate-Limiter paradoxerweise selbst zum Flaschenhals machen kann. Diese Metriken lassen sich über Prometheus-Exporter aus dem PHP-Code oder dem Gateway abgreifen und in Grafana visualisieren.
Alerting-Regeln sollten zwischen zwei Szenarien unterscheiden: einem plötzlichen Anstieg der 429-Rate auf einen einzelnen Endpunkt, was auf einen aktiven Angriff oder einen fehlerhaften Integrationsclient hindeutet, und einer gleichmäßig hohen Ablehnungsrate über viele Clients, die eher auf ein zu striktes globales Limit nach einem Traffic-Wachstum hinweist. Structured Logging jeder Ablehnung mit Client-Identifikator, Endpunkt, Zeitstempel und aktuellem Zähler-Stand ermöglicht nachträgliche forensische Analyse und die Unterscheidung zwischen einmaligen Ausreißern und systematischem Missbrauch, der eine dauerhafte Sperrliste rechtfertigt.
{
"alert": "HighRateLimitRejectionRate",
"expr": "sum(rate(ratelimit_rejected_total[5m])) by (endpoint) > 50",
"for": "2m",
"labels": { "severity": "warning", "team": "platform" },
"annotations": {
"summary": "Elevated 429 rate on {{ $labels.endpoint }}",
"description": "More than 50 rejected requests per second over 5 minutes, check for abuse or misbehaving client."
},
"sample_log_entry": {
"timestamp": "2026-07-12T09:14:32Z",
"event": "rate_limit_rejected",
"client_key": "api-key:integration-erp-01",
"endpoint": "/rest/V1/products",
"limit": 120,
"window_seconds": 60,
"current_count": 121,
"client_ip": "203.0.113.42"
}
}
9. Rate-Limiting-Algorithmen im direkten Vergleich
Jeder Algorithmus löst das Grenzproblem der Anfragenzählung mit unterschiedlichen Kompromissen zwischen Genauigkeit, Speicherbedarf und Implementierungsaufwand. Die folgende Übersicht fasst zusammen, welcher Algorithmus für welchen Anwendungsfall die richtige Wahl ist.
| Algorithmus | Burst-Handling | Speicherbedarf | Genauigkeit | Empfehlung |
|---|---|---|---|---|
| Fixed Window | Doppelte Last an Fenstergrenze möglich | Minimal, 1 Counter | Ungenau an Fenstergrenzen | Nur für einfache, unkritische Endpunkte |
| Sliding Window Counter | Glättet Grenzeffekt zuverlässig | Minimal, 2 Counter | Sehr gute Näherung | Empfohlener Standard für die meisten APIs |
| Token Bucket | Exzellent, native Burst-Unterstützung | Gering, 1 Bucket pro Client | Sehr genau | Empfohlen für APIs mit schwankendem Traffic |
| Sliding Log | Exzellent, exakte Historie | Hoch, Timestamp pro Request | Exakt, keine Näherung | Nur bei kleinem Volumen praktikabel |
In der Praxis kombinieren viele produktive Systeme Token Bucket auf Application-Ebene für granulare, geschäftslogikbewusste Limits mit einem einfacheren Sliding-Window-Counter am Gateway für groben, volumetrischen Schutz. Sliding Log bleibt trotz seiner Exaktheit meist Nischen-Anwendungen mit geringem Traffic-Volumen vorbehalten, während Fixed Window aufgrund seiner Grenzproblematik zunehmend durch Sliding-Window-Varianten ersetzt wird.
Mironsoft
API-Security, Rate-Limiting und Magento-Hardening für produktive Shops
Bereit für belastbares API-Rate-Limiting?
Wir analysieren eure REST- und GraphQL-Endpunkte, identifizieren ungeschützte Ressourcen und implementieren ein Redis-basiertes Rate-Limiting-System, das Missbrauch stoppt, ohne legitime Kunden auszubremsen.
Rate-Limiting-Audit
Analyse eurer Endpunkte und Priorisierung nach Missbrauchsrisiko
Redis-Implementierung
Token-Bucket- und Sliding-Window-Limiter als Magento-Plugin
Monitoring & Alerting
Prometheus-Metriken und Alert-Regeln für Ablehnungsraten
10. Zusammenfassung
API-Rate-Limiting löst ein Grundproblem jeder öffentlichen Schnittstelle: Ohne Kontingente kann ein einzelner Client, egal ob böswillig oder schlicht fehlkonfiguriert, das gesamte System für alle anderen Nutzer ausbremsen. Fixed Window ist einfach, aber ungenau an Fenstergrenzen. Sliding Window Counter und Token Bucket liefern für die meisten APIs die beste Balance aus Genauigkeit, Speicherbedarf und Burst-Toleranz. Die Wahl zwischen Per-IP- und Per-API-Key-Limitierung sollte sich am Authentifizierungsstatus des Endpunkts orientieren, mit einer Kombination beider Ebenen als robusteste Lösung.
Rate-Limit-Header wie X-RateLimit-Remaining und Retry-After verwandeln ein hartes Limit in eine kooperative Kommunikation mit gut implementierten Clients. Die Kombination aus Gateway-Layer für groben, volumetrischen Schutz und Application-Layer für geschäftslogikbewusste Limits, umgesetzt über einen Redis-basierten, atomaren Zähler in Magento, deckt sowohl einfache Massenangriffe als auch gezielten Missbrauch ab. Kontinuierliches Monitoring mit klaren Alerting-Schwellen stellt sicher, dass Limits weder zu Kundenverlust durch Überblockierung noch zu ungeschütztem Overload führen.
API-Rate-Limiting gegen Missbrauch und Overload - Das Wichtigste auf einen Blick
Richtiger Algorithmus
Sliding Window Counter oder Token Bucket statt Fixed Window - bessere Genauigkeit und kontrollierte Burst-Toleranz.
Layered Defense
Grobes Gateway-Limit gegen Volumenangriffe, feingranulares Application-Limit für Geschäftslogik.
Kooperative Clients
X-RateLimit-*- und Retry-After-Header senken unnötige 429-Fehler bei gut implementierten Integrationen.
Kontinuierliches Monitoring
429-Raten pro Endpunkt und Client beobachten, Alerts für Angriffsmuster und zu strikte Limits trennen.