Zum Inhalt springen
Deutsch

Rate-Limit-Middleware

rateLimit ist ein Request-Limiter pro Key. Er gibt einen Handler-Wrapper zurück — rufe ihn mit den Optionen auf und übergib der zurückgegebenen Funktion den Handler, den Du schützen willst. Überschüssige Requests bekommen 429 mit einem Retry-After-Header.

import { rateLimit } from 'actor-ts/http';
import { InMemoryCache } from 'actor-ts';
const limited = rateLimit({
cache: new InMemoryCache(),
windowMs: 60_000, // 1-Minuten-Fenster
max: 100, // 100 Requests pro Key pro Minute
key: (req) => req.headers['x-forwarded-for'] ?? 'unknown',
});
const routes = path('api', post(limited(apiHandler)));

Jede eindeutige IP bekommt ein Budget von 100/Minute. Überschüssige Requests bekommen 429 mit einem Retry-After- Header.

interface RateLimitOptions {
cache: Cache;
windowMs: number;
max: number;
key: (req: HttpRequest) => string | Promise<string>;
keyPrefix?: string; // Default 'rl:'
onLimit?: (ctx: RateLimitContext) => HttpResponse;
}
interface RateLimitContext {
key: string;
count: number;
max: number;
windowMs: number;
retryAfterSeconds: number;
}
FeldZweck
cacheBacking-Store (In-Memory oder Redis).
windowMsGröße des Fixed-Windows.
maxMaximale Requests pro Key pro Fenster.
key(req)Leitet den Rate-Limit-Key her — typischerweise Client-IP, User-ID oder API-Key. Erforderlich.
keyPrefixCache-Key-Namespace. Default 'rl:', damit mehrere Limiter im selben Redis nicht kollidieren.
onLimit(ctx)Überschreibt die 429-Response — volle Kontrolle über Status / Body / Header.
rateLimit({
cache,
windowMs: 60_000,
max: 100,
key: (req) => {
const userId = extractUserId(req);
return userId ? `user:${userId}` : `ip:${req.ip}`;
},
});

Pro User bei Authentifizierung; pro IP, wenn nicht. Häufiges Muster:

  • Authentifizierte Routen — Limit pro User.
  • Öffentliche Routen — Limit pro IP.

Kombiniere mit der Cache-Abstraktion des Frameworks, damit der Limiter über Pods hinweg mit Redis-Backing funktioniert.

Fixed-Window (Default):
- Das Fenster startet, wenn der erste Request für einen Key kommt.
- Inkrementiert einen Zähler im Cache.
- Wenn der Zähler max übersteigt, bekommen weitere Requests
im Fenster 429.
- TTL auf dem Zähler sorgt dafür, dass frische Fenster sauber starten.

Die Implementierung nutzt das incr mit TTL des Caches — atomar über Pods hinweg, wenn Redis im Backing steht.

Die Middleware setzt:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1716297600 ← Unix-Zeitstempel, wann das Fenster zurückgesetzt wird
Retry-After: 23 ← (nur bei 429) Sekunden bis zum Reset

Clients nutzen die, um zu backoffen, statt zufällig zu retrien.

import { RedisCache } from 'actor-ts';
rateLimit({
cache: new RedisCache({ url: 'redis://...' }),
windowMs: 60_000,
max: 100,
});

Mit Redis-gestütztem Cache teilen sich alle Pods den Zähler — ein Client, der irgendeinen Pod trifft, sammelt gegen dasselbe Budget an.

Mit InMemoryCache hat jeder Pod seinen eigenen Zähler — ein Client, der über 4 Pods verteilt ist, bekommt effektiv das 4-fache Limit. Für großzügige Limits akzeptabel; für strikte problematisch.

Drei gute Einsatzfälle:

  1. Öffentliche APIs — Missbrauch / DoS verhindern.
  2. Tier-basierte Limits — unterschiedliche key pro Tier (free / paid / enterprise).
  3. Per-Aktion-Limits — Login-Versuche, Passwort-Resets, E-Mail-Versandraten.
// Unterschiedliche Limits pro Handler:
const apiLimited = rateLimit({
cache, windowMs: 60_000, max: 1000,
key: (req) => req.headers['x-user-id'] ?? req.headers['x-forwarded-for'] ?? 'anon',
});
const loginLimited = rateLimit({
cache, windowMs: 60_000, max: 10,
key: (req) => `login:${req.headers['x-forwarded-for'] ?? 'unknown'}`,
});
const routes = concat(
path('login', post(loginLimited(loginHandler))),
path('orders', get(apiLimited(ordersHandler))),
);

Strengere Limits auf sensiblen Handlern; großzügigere auf allgemeinem Traffic. Komponiere, indem Du jeden Handler mit dem passenden Limiter einwickelst.