Throttling für Magentos Web-API sauber selbst umsetzen
Weder Magento Open Source noch Adobe Commerce On-Premise bringen ein natives Rate-Limiting für die REST-API mit, OAuth-Consumer und Integrationsrechte regeln lediglich, welche Ressourcen ein Client aufrufen darf, nicht wie oft. Dieser Artikel zeigt, wie sich Webserver- und Application-Layer sinnvoll kombinieren lassen, und stellt eine Token-Bucket-Implementierung als Plugin vor, die über Redis auch bei mehreren Webknoten konsistent zählt.
Inhaltsverzeichnis
- 1. Warum Magento kein natives Rate-Limiting mitbringt
- 2. Wo Rate-Limiting ansetzen kann: Webserver-Layer vs. Application-Layer
- 3. Nginx-Level-Rate-Limiting mit limit_req_zone
- 4. Application-Layer-Ansatz: Ein Plugin auf Rest::dispatch
- 5. Den Rate-Limit-Schlüssel identifizieren
- 6. Der Token-Bucket-Algorithmus im Konzept
- 7. Token-Bucket-Implementierung mit Redis und einem Lua-Skript
- 8. Eine korrekte 429-Antwort inklusive Retry-After ausliefern
- 9. Unterschied zu OAuth-Consumer-Limits und Monitoring
- 10. Zusammenfassung
- 11. FAQ
1. Warum Magento kein natives Rate-Limiting mitbringt
Magentos Web-API-Schicht ist konsequent auf Zugriffssteuerung statt Durchsatzsteuerung ausgelegt: OAuth-Consumer, Integrationen und ACL-Rollen entscheiden, welche Ressourcen ein Client aufrufen darf. Keine dieser Schichten begrenzt jedoch, wie oft er das innerhalb welchen Zeitraums tun darf, das ist eine bewusste Lücke im Kern, kein übersehenes Detail.
Durchsatzbegrenzung hängt stark von Infrastruktur, Traffic-Muster und Geschäftsmodell einer konkreten Installation ab. Ein Marktplatz-Integrator mit tausenden Bestellungen pro Minute braucht andere Werte als ein kleiner B2B-Shop mit einer Handvoll Partnersystemen, weshalb Magento diese Verantwortung bewusst an Webserver, Infrastruktur oder eigene Erweiterungen delegiert, statt eine Einheitslösung vorzugeben.
Hinzu kommt, dass Rate-Limiting eng mit Geschäftslogik verzahnt sein kann, etwa wenn ein Vertragspartner vertraglich ein bestimmtes API-Kontingent zugesichert bekommt oder unterschiedliche Integrationen unterschiedlich kritisch für den laufenden Betrieb sind. Eine solche differenzierte Steuerung ließe sich im Kern kaum generisch und gleichzeitig performant abbilden, ohne jede Installation mit ungenutzter Komplexität zu belasten, weshalb die Entscheidung, dies Erweiterungen zu überlassen, auch aus Architektursicht nachvollziehbar bleibt.
2. Wo Rate-Limiting ansetzen kann: Webserver-Layer vs. Application-Layer
Auf Webserver-Ebene lässt sich mit vergleichsweise wenig Aufwand ein grober Schutz gegen Lastspitzen einziehen. Allerdings kennt der Webserver typischerweise nur IP-Adresse oder einfache Header, keine Magento-spezifische Consumer-Identität, weshalb sich Limits pro Integration oder Kunde dort kaum sauber abbilden lassen.
Auf Application-Ebene stehen dagegen die vollen Magento-Konzepte zur Verfügung, OAuth-Consumer-Kennung, Integrationsname, Kundengruppe, was feingranulare, geschäftlich sinnvolle Limits erlaubt, allerdings zusätzliche PHP-Ausführungszeit pro Anfrage kostet. In der Praxis ergänzen sich beide Ebenen meist, statt sich gegenseitig zu ersetzen.
Eine dritte, oft übersehene Ebene ist die Datenbank selbst: Auch ein sauber begrenzter REST-Zugriff kann im Hintergrund teure, langsame Datenbankabfragen auslösen, deren Kosten kein Rate-Limiter auf HTTP-Ebene erfasst. Wer wirklich robust gegen Überlast sein will, kombiniert deshalb Anfrage-Limits auf Webserver- und Application-Ebene mit einer Beobachtung der tatsächlichen Datenbanklast pro Consumer, statt sich allein auf die Anzahl der Requests zu verlassen.
3. Nginx-Level-Rate-Limiting mit limit_req_zone
Ein limit_req_zone-Block definiert eine gemeinsame Zone mit Schlüssel, üblicherweise die Client-IP, und einer Rate. limit_req wendet diese Zone dann auf einen konkreten Location-Block wie /rest/ an, inklusive optionaler burst-Größe für kurzfristige Lastspitzen.
Der klare Vorteil ist die geringe zusätzliche Latenz, weil abgelehnte Anfragen gar nicht erst bis zu PHP-FPM durchdringen. Der Nachteil bleibt die fehlende Consumer-Identität: Ein NAT-Gateway mit vielen dahinterliegenden Nutzern oder ein einzelner Integrations-Client mit mehreren IPs lässt sich so nicht präzise begrenzen.
# Grober Schutz auf Webserver-Ebene, in http {} definieren
limit_req_zone $binary_remote_addr zone=magento_rest:10m rate=20r/s;
server {
location /rest/ {
limit_req zone=magento_rest burst=40 nodelay;
try_files $uri $uri/ /index.php?$args;
}
}
4. Application-Layer-Ansatz: Ein Plugin auf Rest::dispatch
Der zentrale Einstiegspunkt jeder REST-Anfrage ist Magento\Webapi\Controller\Rest::dispatch. Ein Around-Plugin an dieser Stelle sieht jede Anfrage, bevor die eigentliche Service-Contract-Methode ausgeführt wird, und kann bei Überschreitung des Limits die Ausführung abbrechen, ohne den restlichen Webapi-Stack zu verändern.
Diese Lösung bleibt bewusst als Plugin statt als Preference umgesetzt, folgt also derselben Konvention wie der Rest des Projekts, und lässt sich über die reguläre Modul-Sequenz sauber vor oder nach anderen Webapi-Plugins einordnen.
<?php
declare(strict_types=1);
namespace Vendor\ApiRateLimit\Plugin;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\ResponseInterface;
use Magento\Webapi\Controller\Rest;
use Vendor\ApiRateLimit\Model\RateLimitDecision;
use Vendor\ApiRateLimit\Model\RateLimiterKeyResolver;
use Vendor\ApiRateLimit\Model\TokenBucketLimiter;
/**
* Prüft vor jeder REST-Anfrage, ob das Rate-Limit des anfragenden Clients
* bereits erschöpft ist, und bricht die Verarbeitung sonst kontrolliert ab.
*/
class RateLimitPlugin
{
/**
* @param TokenBucketLimiter $limiter
* @param RateLimiterKeyResolver $keyResolver
*/
public function __construct(
private readonly TokenBucketLimiter $limiter,
private readonly RateLimiterKeyResolver $keyResolver
) {
}
/**
* Umschließt Rest::dispatch und wirft bei Ueberschreitung des Limits
* eine Webapi-Exception mit Status 429, bevor die Anfrage verarbeitet wird.
*
* @param Rest $subject
* @param callable $proceed
* @param RequestInterface $request
* @return ResponseInterface
* @throws \Magento\Framework\Webapi\Exception
*/
public function aroundDispatch(Rest $subject, callable $proceed, RequestInterface $request): ResponseInterface
{
$key = $this->keyResolver->resolve($request);
$decision = $this->limiter->consume($key);
if (!$decision->isAllowed()) {
throw new \Magento\Framework\Webapi\Exception(
__('Rate limit exceeded, retry in %1 seconds.', $decision->getRetryAfterSeconds()),
0,
429
);
}
return $proceed($request);
}
}
5. Den Rate-Limit-Schlüssel identifizieren
Für authentifizierte Anfragen liefert der Request die OAuth-Consumer-Kennung oder das Integrationstoken, beides eignet sich deutlich besser als die IP-Adresse, weil eine Integration hinter wechselnden IPs oder mehreren Servern trotzdem konsistent begrenzt werden soll.
Für nicht authentifizierte oder fehlerhafte Anfragen bleibt die IP-Adresse als Fallback sinnvoll, allerdings mit einem deutlich strengeren Kontingent, weil unauthentifizierte Endpunkte typischerweise das primäre Ziel automatisierter Angriffe sind.
6. Der Token-Bucket-Algorithmus im Konzept
Ein Token-Bucket besitzt eine feste Kapazität und eine Nachfüllrate. Jede Anfrage entnimmt ein Token, ist der Eimer leer, wird die Anfrage abgelehnt, andernfalls verringert sich der Füllstand um eins, über die Zeit füllt sich der Eimer wieder bis zur Kapazität auf.
Der entscheidende Vorteil gegenüber einem starren Zeitfenster-Zähler ist, dass kurzfristige Lastspitzen bis zur Kapazität des Eimers weiterhin erlaubt bleiben, während der Durchschnittsdurchsatz über die Nachfüllrate trotzdem strikt begrenzt wird, was echtem Nutzerverhalten deutlich näherkommt als ein hartes Sekundenfenster.
7. Token-Bucket-Implementierung mit Redis und einem Lua-Skript
Weil Lesen, Verringern und Schreiben des Füllstands atomar geschehen müssen, reicht ein einfacher Cache-Zugriff nicht aus. Unter Last entstehen sonst Race Conditions, bei denen mehrere parallele Anfragen denselben, längst verbrauchten Füllstand lesen und fälschlich alle durchlassen.
Redis führt ein Lua-Skript als einzelne, unteilbare Operation aus, wodurch Lesen und Schreiben des Füllstands garantiert nicht von einer parallelen Anfrage unterbrochen werden. Das funktioniert auch dann korrekt, wenn mehrere Magento-Webknoten gleichzeitig gegen denselben Redis-Zähler arbeiten.
-- KEYS[1] = Bucket-Schlüssel, ARGV[1] = Kapazität, ARGV[2] = Nachfüllrate pro Sekunde,
-- ARGV[3] = aktueller Unix-Timestamp, ARGV[4] = angefragte Tokens (meist 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", KEYS[1], "tokens", "updated_at")
local tokens = tonumber(bucket[1]) or capacity
local updated_at = tonumber(bucket[2]) or now
local elapsed = math.max(0, now - updated_at)
tokens = math.min(capacity, tokens + elapsed * refill_rate)
if tokens < requested then
redis.call("HMSET", KEYS[1], "tokens", tokens, "updated_at", now)
redis.call("EXPIRE", KEYS[1], 3600)
return {0, math.ceil((requested - tokens) / refill_rate)}
end
tokens = tokens - requested
redis.call("HMSET", KEYS[1], "tokens", tokens, "updated_at", now)
redis.call("EXPIRE", KEYS[1], 3600)
return {1, 0}
8. Eine korrekte 429-Antwort inklusive Retry-After ausliefern
Magento\Framework\Webapi\Exception transportiert sowohl eine Nachricht als auch einen expliziten HTTP-Status-Code. Für eine Rate-Limit-Verletzung eignet sich Status 429 zusammen mit einem Retry-After-Header, der die Sekunden bis zur nächsten sinnvollen Anfrage angibt.
Ein gut erzogener Client kann diesen Header auswerten und automatisch mit passendem Abstand erneut anfragen. Ein stilles Verwerfen der Anfrage ohne aussagekräftigen Status zwingt jeden Client dagegen zu eigenem, oft ungeeignetem Retry-Verhalten.
<?php
declare(strict_types=1);
namespace Vendor\ApiRateLimit\Model;
/**
* Repräsentiert das Ergebnis einer Token-Bucket-Prüfung für einen Rate-Limit-Schlüssel.
*/
final class RateLimitDecision
{
/**
* @param bool $allowed
* @param int $retryAfterSeconds
*/
public function __construct(private readonly bool $allowed, private readonly int $retryAfterSeconds)
{
}
/**
* @return bool
*/
public function isAllowed(): bool
{
return $this->allowed;
}
/**
* @return int
*/
public function getRetryAfterSeconds(): int
{
return $this->retryAfterSeconds;
}
}
9. Unterschied zu OAuth-Consumer-Limits und Monitoring
OAuth-Consumer und Integrationsrechte beantworten die Frage, welche Ressourcen ein Client überhaupt aufrufen darf, ein Rate-Limiter beantwortet die davon unabhängige Frage, wie oft er das in welchem Zeitraum tun darf. Beide Mechanismen ergänzen sich, keiner ersetzt den anderen.
Abgelehnte Anfragen sollten mit Consumer-Kennung, Endpunkt und Zeitstempel geloggt werden. Ein Anstieg der Ablehnungsrate nach einem Integrationsrelease deutet meist auf ein zu knappes Limit hin, während ein plötzlicher Anstieg von vielen unterschiedlichen, unauthentifizierten Quellen eher auf einen tatsächlichen Angriffsversuch hindeutet.
Sinnvoll ist außerdem ein Dashboard, das die aktuelle Auslastung jedes Kontingents in Relation zu seiner Kapazität zeigt, statt nur harte Ablehnungen zu zählen. So lässt sich frühzeitig erkennen, wenn eine Integration sich einem Limit nähert, noch bevor tatsächlich Anfragen abgelehnt werden, und ein Gespräch mit dem Integrationspartner über eine Kontingentanpassung kann proaktiv statt reaktiv geführt werden. Gerade bei extern betriebenen Integrationen zahlt sich diese Transparenz aus, weil sich ein knapper werdendes Kontingent oft Wochen vor dem eigentlichen Engpass ankündigt.
Bei der Einführung von Rate-Limiting in einem gewachsenen System empfiehlt sich außerdem eine Übergangsphase im reinen Beobachtungsmodus, in der Überschreitungen zwar erkannt und geloggt, aber noch nicht tatsächlich blockiert werden. So lassen sich realistische Grenzwerte anhand des tatsächlichen Verhaltens bestehender Integrationen festlegen, statt Limits allein auf Schätzungen zu stützen und produktive Integrationen versehentlich lahmzulegen.
| Ebene | Werkzeug | Kennt OAuth-Consumer? | Typischer Einsatz |
|---|---|---|---|
| Webserver | Nginx limit_req_zone | Nein, nur IP oder Header | Grobschutz vor der Anwendung |
| Application (Plugin) | Around-Plugin auf Rest::dispatch | Ja, über Request-Header oder Consumer-ID | Feingranulares Limit pro Kunde oder Integration |
| Application (Token Bucket) | Redis plus Lua-Skript | Ja | Präzise, atomare Zähler über mehrere Web-Knoten hinweg |
| Infrastruktur oder CDN | Fastly- oder Cloud-Edge-Regeln | Teilweise, meist nur über Header-Weitergabe | Schutz vor Layer-7-Lastspitzen |
| OAuth-Integration (Admin) | Integrationsrechte und ACL | Ja, aber kein Rate-Limit | Zugriffssteuerung, nicht Durchsatzbegrenzung |
Mironsoft
Magento-Entwicklung, Modul-Beratung und Systemarchitektur
Magento-Projekt, das eine zweite Meinung oder erfahrene Umsetzung braucht?
Wir entwickeln individuelle Magento-Module, beraten bei Architekturentscheidungen und übernehmen komplexe Umsetzungen, von der Service-Contract-Planung bis zum produktionsreifen Deployment.
Architektur-Beratung
Modul- und Systemarchitektur vor der Umsetzung fundiert durchdenken lassen.
Custom-Modul-Entwicklung
Individuelle Magento-Module nach Best Practices sauber umsetzen.
Code-Review & Audit
Bestehende Module auf Performance, Sicherheit und Wartbarkeit prüfen lassen.
10. Zusammenfassung
REST-API Rate Limiting in Magento: Das Wichtigste auf einen Blick
Ausgangslage
Magento bietet keinerlei natives Rate-Limiting für REST, OAuth-Consumer regeln nur Zugriffsrechte, keinen Durchsatz.
Empfohlene Schicht
Application-Layer-Plugin auf dem Webapi-Dispatch, ergänzt um grobes Nginx-Limiting davor.
Algorithmus
Token Bucket mit atomarem Redis-Lua-Skript für konsistente Zählung über mehrere Web-Knoten.
Antwortverhalten
Bei Überschreitung HTTP 429 mit Retry-After-Header statt stillem Verwerfen der Anfrage.