Rate-Limiting-Muster für APIs, die skalieren
Token Buckets, Sliding Windows und verteiltes Rate Limiting — praktische Muster, um deine API zu schützen, ohne legitime Nutzer auszubremsen.

Jede öffentliche API braucht Rate Limiting. Ohne es kann ein einzelner sich falsch verhaltender Client deine Datenbank sättigen, dein Compute-Budget aufbrauchen oder den gesamten Dienst lahmlegen. Aber naives Rate Limiting — ein simpler Zähler, der jede Minute zurückgesetzt wird — erzeugt Klippeneffekte und bestraft legitimen Burst-Traffic.
Fixed Window: einfach, aber fehlerhaft
Der einfachste Ansatz: Requests pro Zeitfenster zählen. Überschreitet der Zähler das Limit, wird abgelehnt.
// ❌ Fixed window — boundary burst problem
async function fixedWindowLimit(
key: string,
limit: number,
windowMs: number,
): Promise<boolean> {
const window = Math.floor(Date.now() / windowMs);
const counterKey = `ratelimit:${key}:${window}`;
const count = await redis.incr(counterKey);
if (count === 1) await redis.pexpire(counterKey, windowMs);
return count <= limit;
}
// Problem: at 11:59:59, a client sends 100 requests (under the limit).
// At 12:00:01, they send another 100. Both pass — 200 requests in 2 seconds.Das Grenzproblem bedeutet, dass ein Client sein effektives Rate Limit verdoppeln kann, indem er Requests über Fenstergrenzen hinweg timt.
Sliding-Window-Log
Zeichne einzelne Request-Zeitstempel auf und zähle, wie viele in das gleitende Fenster fallen. Genauer, verbraucht aber mehr Speicher.
async function slidingWindowLog(
key: string,
limit: number,
windowMs: number,
): Promise<boolean> {
const now = Date.now();
const windowStart = now - windowMs;
const sortedSetKey = `ratelimit:${key}`;
// Remove expired entries and add current request atomically
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(sortedSetKey, 0, windowStart);
pipeline.zadd(sortedSetKey, now, `${now}:${Math.random()}`);
pipeline.zcard(sortedSetKey);
pipeline.pexpire(sortedSetKey, windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
return count <= limit;
}Das eliminiert das Grenzproblem, speichert aber einen Eintrag pro Request. Bei Endpoints mit hohem Traffic ist das eine Menge Redis-Speicher.
Token Bucket: die beste Standardwahl
Token Buckets erlauben Bursts, während sie eine durchschnittliche Rate durchsetzen. Tokens werden mit einer konstanten Rate hinzugefügt; jeder Request verbraucht ein Token. Ist der Bucket leer, werden Requests abgelehnt.
// ✅ Token bucket — allows bursts, enforces average rate
async function tokenBucket(
key: string,
capacity: number,
refillRate: number, // tokens per second
): Promise<{ allowed: boolean; remaining: number }> {
const bucketKey = `bucket:${key}`;
const now = Date.now();
// Lua script for atomicity
const script = `
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or tonumber(ARGV[1])
local lastRefill = tonumber(bucket[2]) or tonumber(ARGV[3])
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local elapsed = (now - lastRefill) / 1000
tokens = math.min(capacity, tokens + elapsed * refillRate)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(capacity / refillRate) * 1000)
return {allowed, math.floor(tokens)}
`;
const [allowed, remaining] = (await redis.eval(
script,
1,
bucketKey,
capacity,
refillRate,
now,
)) as [number, number];
return { allowed: allowed === 1, remaining };
}Ein Bucket mit Kapazität 100 und einer Auffüllrate von 10/Sekunde erlaubt einen Burst von 100 Requests und hält danach 10/Sekunde. Das entspricht realen Nutzungsmustern besser als Fixed Windows.
Eine Strategie wählen
| Algorithmus | Burst-Handling | Speicher | Genauigkeit | Komplexität |
|---|---|---|---|---|
| Fixed Window | Schlecht (Grenzproblem) | Niedrig | Niedrig | Einfach |
| Sliding-Window-Log | Gut | Hoch | Hoch | Mittel |
| Sliding-Window-Counter | Gut | Niedrig | Mittel | Mittel |
| Token Bucket | Ausgezeichnet | Niedrig | Hoch | Mittel |
Token Bucket ist für die meisten APIs die richtige Standardwahl. Fixed Window ist für interne Services akzeptabel, bei denen Präzision keine Rolle spielt.
Response-Header und 429-Handling
Kommuniziere den Rate-Limit-Status immer über Header. Wohlerzogene Clients nutzen diese, um sich selbst zu drosseln.
function rateLimitResponse(
res: Response,
limit: number,
remaining: number,
resetAt: number,
) {
res.setHeader("X-RateLimit-Limit", limit);
res.setHeader("X-RateLimit-Remaining", Math.max(0, remaining));
res.setHeader("X-RateLimit-Reset", Math.ceil(resetAt / 1000));
if (remaining < 0) {
res.setHeader("Retry-After", Math.ceil((resetAt - Date.now()) / 1000));
return res.status(429).json({
error: {
code: "RATE_LIMITED",
message:
"Too many requests. Please retry after the Retry-After period.",
},
});
}
}Gestaffelte Rate Limits
Unterschiedliche Endpoints haben unterschiedliche Kostenprofile. Ein Such-Endpoint, der einen Volltextindex trifft, ist teurer als ein Status-Check.
const rateLimits = {
"GET /api/status": { capacity: 1000, refillRate: 100 },
"GET /api/search": { capacity: 20, refillRate: 5 },
"POST /api/orders": { capacity: 10, refillRate: 2 },
"POST /api/auth/login": { capacity: 5, refillRate: 1 },
} as const;
function getRateLimit(method: string, path: string) {
const key = `${method} ${path}`;
return rateLimits[key] ?? { capacity: 100, refillRate: 20 };
}Authentifizierungs-Endpoints verdienen die strengsten Limits — sie sind das primäre Ziel für Brute-Force-Angriffe.
Die wichtigsten Punkte
- Token Bucket ist die beste Standardwahl — es handhabt Bursts auf natürliche Weise, während es durchschnittliche Raten durchsetzt
- Fixed Windows haben ein Grenzproblem — Clients können ihre effektive Rate an Fenstergrenzen verdoppeln
- Nutze Lua-Skripte in Redis für atomare mehrstufige Rate-Limiting-Operationen
- Gib immer Rate-Limit-Header zurück —
X-RateLimit-RemainingundRetry-Afterhelfen Clients, sich selbst zu drosseln - Staffle Rate Limits nach Endpoint-Kosten — teure Operationen bekommen strengere Limits
- Authentifizierungs-Endpoints brauchen die strengsten Limits — sie sind das primäre Brute-Force-Ziel


