„Rate Limiting hinzufügen“ ist ein Einzeiler-Ticket, das drei verschiedene Probleme verbirgt. Jemand hämmert von einer IP auf die Login-Route: Das ist Missbrauch, und Sie wollen ihn stoppen, bevor er Sie Compute kostet. Ein Integrationspartner ruft die API zu schnell für seinen Plan auf: Das ist Fairness, und Sie wollen ihn bremsen und ihm sagen, warum. Ein Nutzer versucht, tausend Bestellungen pro Minute aufzugeben: Das ist eine Geschäftsregel, und Sie wollen, dass die Anwendung auf eine Weise Nein sagt, die das Produkt versteht.
Jedes Problem hat eine Schicht, in der es billig und richtig zu lösen ist, und eine Schicht, in der es teuer oder falsch ist. So haben wir es für eine öffentliche API auf AWS aufgeteilt, mit Zahlen, Code und der einen Stelle, an der wir bewusst kein Rate Limiting machen.
Die drei Schichten
Der Rand sieht IPs, Pfade und Header, und nichts darüber, wer der Nutzer ist. Er ist der billigste Ort, einen Request zu verwerfen, weil es vor jedem Compute passiert, den Sie bezahlen, und der stumpfste, weil alle hinter einem Firmen-NAT eine IP teilen. Seine Aufgabe ist Missbrauch: Credential Stuffing, Scraper, ein fehlkonfigurierter Client in einer Retry-Schleife.
Das Gateway sieht einen API-Key, wenn Sie welche ausgeben, und kann eine Rate pro Key durchsetzen. Es weiß, welcher Client aufruft, nicht welcher Nutzer, und nicht, was der Request bedeutet. Seine Aufgabe ist Fairness zwischen Integratoren: Niemandes außer Kontrolle geratenes Skript verschlechtert die API für alle anderen.
Die Anwendung sieht den authentifizierten Nutzer, seinen Plan, den Mandanten, zu dem er gehört, und was der Request zu tun versucht. Sie ist die einzige Schicht, die sagen kann „Sie haben in dieser Stunde 50 Bestellungen aufgegeben, Ihr Plan erlaubt 50, versuchen Sie es um 15:00 erneut“. Ihre Aufgabe sind Geschäftslimits, und sie ist die einzige Schicht, die einen Fehler zurückgeben kann, den das Produkt erklären kann.
Schicht 1: die WAF-Regel auf Ratenbasis
Wir haben die WAF-Konfiguration separat behandelt; die Ratenregel ist der Teil davon, der echte Angriffe gestoppt hat. Zwei Regeln, weil ein Limit für die ganze Site falsch ist:
// 2.000 Requests pro 5 Minuten von einer IP, über alles
{ name: 'rate-all', priority: 40,
statement: { rateBasedStatement: { limit: 2000, evaluationWindowSec: 300, aggregateKeyType: 'IP' } },
action: { block: { customResponse: { responseCode: 429 } } } },
// 100 pro 5 Minuten auf Authentifizierungsrouten, wo ein „Nutzer“ vielleicht 5 macht
{ name: 'rate-auth', priority: 41,
statement: { rateBasedStatement: { limit: 100, evaluationWindowSec: 300, aggregateKeyType: 'IP',
scopeDownStatement: { byteMatchStatement: { fieldToMatch: { uriPath: {} }, positionalConstraint: 'STARTS_WITH', searchString: '/api/auth/', textTransformations: [{ priority: 0, type: 'LOWERCASE' }] } } } },
action: { block: { customResponse: { responseCode: 429 } } } },
Zwei Details. Die Custom Response macht den Block zu einem 429 statt WAFs Standard-403, damit Clients, die Rate Limiting verstehen, sich korrekt verhalten. Und das Fenster sind fünf Minuten mit einem Limit von 100 auf Auth, dem ein echter Nutzer nie nahekommt und das ein Credential-Stuffing-Skript in den ersten zehn Sekunden erreicht. In sechs Monaten hat diese Regel drei solcher Versuche und einen Scraper blockiert. Sie kostet $1 im Monat.
Was sie nicht kann: hundert Nutzer hinter einer Büro-IP von einem Angreifer unterscheiden. Als die gesamte Firma eines Kunden vom Login ausgesperrt war, weil alle um 9:00 ankamen, war die Lösung nicht, das Limit zu erhöhen; sie war ein Scope-Down, das deren bekannten Egress-Bereich ausnimmt, eine zweizeilige Änderung und ein Gespräch mit deren IT-Team.
Schicht 2: API-Gateway-Usage-Plans, und warum wir sie nicht nutzen
Sitzt Ihre API hinter API Gateway und geben Sie Keys an Integratoren aus, sind Usage Plans das richtige Werkzeug: eine Burst- und eine Dauerrate pro Key, vom Gateway durchgesetzt, mit 429 ohne geschriebenen Code. Das würden wir für ein öffentliches API-Produkt mit zahlenden Integratoren auf Stufen nutzen.
Wir haben diese Form nicht. Unsere API wird von unseren eigenen Frontends und einer Handvoll Partnern aufgerufen, und sie läuft auf App Runner, nicht hinter API Gateway. Ein Gateway nur für Rate Limiting hinzuzufügen würde einen Hop, Kosten ($3,50 pro Million Requests, mehr als die WAF) und ein 29-Sekunden-Timeout hinzufügen. Also ist die Aufgabe „Fairness zwischen Clients“ in die Anwendungsschicht gewandert, geschlüsselt nach der Identität des Partners statt nach einem API-Key. Hätten wir fünfzig Integratoren statt fünf, würde die Rechnung kippen und wir würden das Gateway einbauen.
Schicht 3: die Anwendung
Der Anwendungs-Limiter ist nach dem geschlüsselt, worum es in der Geschäftsregel geht: Nutzer, Mandant, Partner, manchmal die Ressource. Es ist ein Token Bucket in DynamoDB, weil das ein Speicher ist, den wir bereits haben, atomar ist und bei unserer Request-Rate billig. Ein Item pro Schlüssel, beim Lesen nachgefüllt:
// lib/rate-limit.ts
export async function take(key: string, plan: { capacity: number; refillPerSec: number }): Promise<Allow | Deny> {
const now = Date.now() / 1000;
const res = await ddb.update({
TableName: 'rate-limits', Key: { pk: key },
// bis zur Kapazität anhand der vergangenen Zeit nachfüllen, dann ein Token nehmen
UpdateExpression: 'SET tokens = :cap - :one, updatedAt = :now',
ConditionExpression: 'attribute_not_exists(pk) OR (tokens + (:now - updatedAt) * :refill) >= :one',
ExpressionAttributeValues: { ':cap': plan.capacity, ':one': 1, ':now': now, ':refill': plan.refillPerSec },
ReturnValues: 'ALL_NEW',
}).catch(e => e.name === 'ConditionalCheckFailedException' ? null : Promise.reject(e));
if (!res) return { allowed: false, retryAfterSec: Math.ceil(1 / plan.refillPerSec) };
return { allowed: true, remaining: res.Attributes.tokens };
}
Die echte Version ist ein paar Zeilen länger, weil DynamoDBs Update-Ausdrücke die volle Nachfüll-Arithmetik ohne min() nicht in einer Anweisung schaffen, also ist es Lesen, Rechnen, bedingtes Schreiben mit Retry bei Konflikt. Der Punkt ist die Form: eine atomare Operation pro Request, kein Redis zu betreiben, Items laufen per TTL ab, sodass ruhende Schlüssel nichts kosten.
Die Antwort bei Ablehnung ist das, was diese Schicht ihren Preis wert macht:
HTTP/1.1 429 Too Many Requests
Retry-After: 6
RateLimit-Limit: 50
RateLimit-Remaining: 0
RateLimit-Reset: 6
Content-Type: application/json
{ "error": "rate_limited", "message": "Ihr Plan erlaubt 50 Bestellungen pro Stunde. Versuchen Sie es in 6 Sekunden erneut oder upgraden Sie, um das Limit aufzuheben.", "upgradeUrl": "/billing" }
Ein 429, das das Produkt darstellen kann. Die WAF kann diese Nachricht nicht schreiben, weil sie nicht weiß, was eine Bestellung ist.
Wo wir bewusst kein Rate Limiting machen
Webhook-Empfänger. Ein Zahlungsanbieter, der eine Zustellung wiederholt, ist kein Missbrauch, sondern das Protokoll bei der Arbeit, und ein Schwall von hundert Webhooks nach dessen Ausfall ist genau der Moment, in dem Sie jeden einzelnen annehmen müssen. Die Webhook-Routen sind per Signatur authentifiziert, per Pfad von der WAF-Ratenregel ausgenommen, und die Anwendung legt sie in keinen Bucket. Sind sie ein Lastproblem, ist die Lösung eine Queue hinter dem Empfänger, kein Limit davor.
Health Checks, aus demselben Grund, und weil ein ratenlimitierter Health Check ein Health Check ist, der lügt.
Was es kostet, und was es gefangen hat
| Schicht | Monatliche Kosten | Was es in sechs Monaten gestoppt hat |
|---|---|---|
| WAF-Ratenregeln (2) | ~$2 | 3 Credential-Stuffing-Läufe, 1 Scraper, 1 fehlkonfigurierter Monitoring-Bot |
| API-Gateway-Usage-Plans | $0 (nicht genutzt) | n/a |
| Anwendungs-Limiter (DynamoDB) | ~$1 an On-Demand-Schreibvorgängen | 2 Partner-Retry-Schleifen, ~40 Nutzer am Tag an Planlimits, was das Produkt bei der Arbeit ist |
Fünf Dollar im Monat, ein Nachmittag für die WAF-Regeln, ein Tag für den Anwendungs-Limiter und seinen 429-Body. Die wertvollste Zeile ist die letzte: vierzig Nutzer am Tag, die eine Nachricht sehen, die sagt, was das Limit ist und wie man es aufhebt, statt eines generischen Fehlers, weil das Limit in der Schicht lebt, die weiß, was es bedeutet.
Wenn Sie ein Rate Limit für alles haben und es entweder zu locker ist, um Missbrauch zu stoppen, oder zu eng für echte Nutzer, ist das das Zeichen, dass es in der falschen Schicht sitzt. Wir helfen Ihnen, es aufzuteilen.