Jede produktive API, die im Internet exponiert wird, benötigt einen soliden Rate-Limiting-Mechanismus. Ohne ihn sind Dienste anfällig für Missbrauch, DDoS-Angriffe, unkontrollierte Kostenexplosion und Serviceausfälle. Gerade KI-APIs wie Anthropics Claude Code API verursachen bei unkontrolliertem Zugriff rapide Kosten.

In diesem Artikel zeigen wir die wichtigsten Rate-Limiting-Algorithmen, vergleichen sie anhand konkreter Metriken und implementieren sie mit TypeScript, Redis und gängigen API-Gateway-Lösungen. Alle Codebeispiele sind produktionsreif und direkt einsetzbar.

1. Rate Limiting Algorithmen im Vergleich

Nicht jeder Rate-Limiting-Algorithmus passt zu jedem Anwendungsfall. Die Wahl des richtigen Algorithmus hängt von Faktoren wie Traffic-Muster, Burst-Toleranz, Speicherverbrauch und der Genauigkeit der Limiterkennung ab. Hier ein strukturierter Vergleich der fünf gängigsten Ansätze:

Fixed Window Counter

EINFACH

Der einfachste Ansatz: Für jedes Zeitfenster (z. B. 60 Sekunden) wird ein Zähler hochgezählt. Erreicht der Zähler das Limit, werden weitere Anfragen abgelehnt. Am Ende des Fensters wird der Zähler zurückgesetzt.

✓ Vorteile

  • Minimal Speicherverbrauch
  • Sehr einfach implementierbar
  • Performant (O(1))

✗ Nachteile

  • Burst am Fensterübergang (2x Limit)
  • Ungleichmäßige Verteilung
  • Nicht präzise für Hochlast

Sliding Window Log

PRÄZISE

Jede Anfrage wird mit Timestamp gespeichert. Beim neuen Request werden alle Einträge außerhalb des gleitenden Fensters entfernt. Dann wird gezählt, ob das Limit überschritten ist. Sehr präzise, aber speicherintensiv bei hohem Traffic.

✓ Vorteile

  • Exakt präzise
  • Kein Burst-Problem
  • Ideal für niedrigen Traffic

✗ Nachteile

  • Hoher Speicherverbrauch
  • O(n) bei Aufräumen
  • Schlecht skalierbar

Sliding Window Counter

EMPFOHLEN

Hybrid aus Fixed Window und Sliding Window Log. Kombiniert den aktuellen Fensterzähler mit einem gewichteten Anteil des vorherigen Fensters. Bietet eine sehr gute Balance zwischen Genauigkeit und Performance.

✓ Vorteile

  • Geringer Speicherverbrauch
  • Annähernd präzise
  • Produktionstauglich

✗ Nachteile

  • Leichte Ungenauigkeit
  • Komplexere Logik
  • Schätzung, nicht exakt

Token Bucket

BURST-TOLERANT

Ein virtueller Eimer wird mit Tokens gefüllt (mit konstanter Rate). Jede Anfrage verbraucht einen Token. Ist der Eimer leer, wird die Anfrage abgelehnt oder verzögert. Ideal für APIs, die kurze Bursts erlauben, aber einen Durchschnittsdurchsatz einhalten müssen.

✓ Vorteile

  • Burst-Traffic erlaubt
  • Flexible Konfiguration
  • Gut für Claude Code API

✗ Nachteile

  • State muss persistent sein
  • Refill-Timing kritisch
  • Distributed schwieriger

Leaky Bucket

KONSTANT

Anfragen füllen einen Eimer (Queue). Der Eimer "leckt" mit konstanter Rate — Anfragen werden gleichmäßig verarbeitet. Überschreitet die Queue ihre Kapazität, werden neue Anfragen abgewiesen. Erzwingt einen gleichmäßigen Ausgabestrom.

✓ Vorteile

  • Gleichmäßiger Output-Strom
  • Schützt Downstream-Dienste
  • Vorhersehbares Verhalten

✗ Nachteile

  • Kein Burst erlaubt
  • Latenz durch Queue
  • Requests können warten

Algorithmen-Vergleichsmatrix

Algorithmus Speicher Burst Präzision Empfehlung
Fixed Window O(1) ✗ Problem Niedrig Einfache Dienste
Sliding Window Log O(n) ✓ Kein Problem Hoch Low-Traffic APIs
Sliding Window Counter O(1) ✓ Teilweise Gut Produktion ✓
Token Bucket O(1) ✓ Erlaubt Gut KI-APIs ✓
Leaky Bucket O(n Queue) ✗ Keiner Exakt Backend-Throttling

2. Token Bucket — TypeScript-Implementierung

Das Token-Bucket-Muster eignet sich besonders gut für KI-APIs wie Claude Code, weil es kurze Bursts (z. B. beim Start einer Agentic-Session) erlaubt, während der Durchschnittsdurchsatz kontrolliert bleibt. Hier eine vollständige TypeScript-Klasse mit In-Memory-State:

TokenBucket Klasse (TypeScript)

TOKEN BUCKET
// token-bucket.ts — Produktionsreife Implementierung interface TokenBucketConfig { capacity: number; // Max. Tokens im Eimer refillRate: number; // Tokens pro Sekunde initialTokens?: number; // Start-Tokens (default = capacity) } interface ConsumeResult { allowed: boolean; remaining: number; retryAfterMs: number; } class TokenBucket { private tokens: number; private lastRefill: number; private readonly capacity: number; private readonly refillRate: number; constructor(config: TokenBucketConfig) { this.capacity = config.capacity; this.refillRate = config.refillRate; this.tokens = config.initialTokens ?? config.capacity; this.lastRefill = Date.now(); } private refill(): void { const now = Date.now(); const elapsed = (now - this.lastRefill) / 1000; // Sekunden const newTokens = elapsed * this.refillRate; this.tokens = Math.min( this.capacity, this.tokens + newTokens ); this.lastRefill = now; } consume(tokens: number = 1): ConsumeResult { this.refill(); if (this.tokens >= tokens) { this.tokens -= tokens; return { allowed: true, remaining: Math.floor(this.tokens), retryAfterMs: 0 }; } // Berechne Wartezeit bis genug Tokens vorhanden const deficit = tokens - this.tokens; const retryAfterMs = (deficit / this.refillRate) * 1000; return { allowed: false, remaining: Math.floor(this.tokens), retryAfterMs: Math.ceil(retryAfterMs) }; } peek(): number { this.refill(); return Math.floor(this.tokens); } }

Die refill()-Methode wird lazy aufgerufen — nur wenn Tokens angefragt werden. Das vermeidet unnötige Intervall-Timers und macht die Klasse thread-safe für Node.js (single-threaded).

Bucket Registry für Multi-User-APIs

In echten APIs benötigst du einen Bucket pro API-Key oder User-ID. Eine einfache Map-basierte Registry mit automatischem Cleanup verwaltet die Buckets effizient:

BucketRegistry — Multi-Tenant Rate Limiting

MULTI-TENANT
class TokenBucketRegistry { private buckets = new Map<string, { bucket: TokenBucket; lastUsed: number }>(); private readonly config: TokenBucketConfig; private readonly ttlMs: number; constructor(config: TokenBucketConfig, ttlMs = 3_600_000) { this.config = config; this.ttlMs = ttlMs; // Cleanup alle 10 Minuten setInterval(() => this.cleanup(), 10 * 60 * 1000); } getBucket(key: string): TokenBucket { const entry = this.buckets.get(key); if (entry) { entry.lastUsed = Date.now(); return entry.bucket; } const bucket = new TokenBucket(this.config); this.buckets.set(key, { bucket, lastUsed: Date.now() }); return bucket; } private cleanup(): void { const cutoff = Date.now() - this.ttlMs; for (const [key, entry] of this.buckets) { if (entry.lastUsed < cutoff) { this.buckets.delete(key); } } } } // Verwendung: const registry = new TokenBucketRegistry({ capacity: 100, // 100 Requests Burst refillRate: 10 // 10 Requests/Sekunde steady-state }); const result = registry.getBucket('user:abc123').consume(); if (!result.allowed) { // 429 Too Many Requests senden console.log(`Retry after: ${result.retryAfterMs}ms`); }

Claude Code Empfehlung: Setze capacity = 20 und refillRate = 2 für Agentic-Sessions. Das erlaubt kurze Tool-Use-Bursts (bis 20 parallele Calls) bei einem Steady-State von 2 Requests/Sekunde pro API-Key — passend zu Anthropics Rate Limits.

3. Redis Sliding Window mit Lua-Script

Für distributed APIs, bei denen mehrere Server-Instanzen gleichzeitig laufen, reicht In-Memory-State nicht aus. Redis bietet die perfekte Basis für ein atomares Sliding-Window durch ZADD, ZREMRANGEBYSCORE und ZCARD — alles in einem atomaren Lua-Script.

Redis Sliding Window — Datenstruktur

REDIS
// Konzept: Sorted Set pro User-Key // Key: "rl:{userId}:{windowKey}" // Score: Unix-Timestamp in Millisekunden // Member: unique Request-ID (nanoid) // ZADD: Neuen Request eintragen // ZREMRANGEBYSCORE: Alte Requests außerhalb des Fensters entfernen // ZCARD: Aktuelle Anzahl zählen // EXPIRE: Key automatisch ablaufen lassen // Beispiel-State für User "user:abc" (100req/60s Limit): // Key: "rl:user:abc:60" // Members: [req_1714900001234, req_1714900023456, ...] // Scores: [1714900001234, 1714900023456, ...]

Atomares Lua-Script für Race-Condition-Sicherheit

Das kritischste Problem bei verteilten Rate Limitern ist die Race Condition zwischen "prüfen" und "eintragen". Ein Lua-Script in Redis löst das atomar:

Lua-Script — Atomic Check-and-Increment

LUA ATOMIC
-- sliding_window.lua local key = KEYS[1] local now = tonumber(ARGV[1]) -- aktueller Timestamp (ms) local window = tonumber(ARGV[2]) -- Fenster in ms (z.B. 60000) local limit = tonumber(ARGV[3]) -- Max. Requests pro Fenster local request_id = ARGV[4] -- Unique ID für diesen Request -- 1. Alte Requests entfernen (außerhalb des Fensters) redis.call('ZREMRANGEBYSCORE', key, 0, now - window) -- 2. Aktuelle Anzahl prüfen local count = redis.call('ZCARD', key) if count < limit then -- 3. Request eintragen redis.call('ZADD', key, now, request_id) -- 4. Key-Expiry setzen (2x Fenster als Sicherheit) redis.call('PEXPIRE', key, window * 2) return {1, limit - count - 1, 0} -- allowed, remaining, retryAfter else -- 5. Ältesten Request finden → Retry-After berechnen local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES') local retry_after = 0 if #oldest > 0 then retry_after = window - (now - tonumber(oldest[2])) end return {0, 0, retry_after} -- denied, remaining=0, retryAfter(ms) end

TypeScript-Wrapper für das Lua-Script

RedisRateLimiter — TypeScript Client

REDIS CLIENT
import { createClient } from 'redis'; import { nanoid } from 'nanoid'; import { readFileSync } from 'fs'; const luaScript = readFileSync('./sliding_window.lua', 'utf-8'); interface RateLimitResult { allowed: boolean; remaining: number; retryAfterMs: number; limit: number; windowMs: number; } class RedisRateLimiter { private client: ReturnType<typeof createClient>; private scriptSha?: string; constructor(redisUrl: string) { this.client = createClient({ url: redisUrl }); this.client.on('error', (err) => console.error('Redis Rate Limiter Error:', err) ); } async connect(): Promise<void> { await this.client.connect(); // Script vorher laden (EVALSHA statt EVAL = performance) this.scriptSha = await this.client.scriptLoad(luaScript); } async check( identifier: string, limit: number, windowMs: number ): Promise<RateLimitResult> { const key = `rl:${identifier}:${windowMs}`; const now = Date.now(); const requestId = nanoid(); const result = await this.client.evalSha(this.scriptSha!, { keys: [key], arguments: [ now.toString(), windowMs.toString(), limit.toString(), requestId ] }) as number[]; return { allowed: result[0] === 1, remaining: result[1], retryAfterMs: result[2], limit, windowMs }; } }

4. Express Middleware Integration

In Node.js-Applikationen ist Express der Standard-Webserver. Mit express-rate-limit und rate-limit-redis lässt sich ein produktionsreifer Rate Limiter in wenigen Zeilen aufsetzen. Wichtig: Immer die richtigen HTTP-Headers setzen, damit Clients sauber reagieren können.

express-rate-limit + Redis Store

EXPRESS
import express from 'express'; import rateLimit from 'express-rate-limit'; import { RedisStore } from 'rate-limit-redis'; import { createClient } from 'redis'; const app = express(); const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' }); await redisClient.connect(); // Standard API Limiter const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 Minuten max: 100, // 100 Requests pro Fenster standardHeaders: true, // RateLimit-* Headers (RFC 6585) legacyHeaders: false, store: new RedisStore({ sendCommand: (...args: string[]) => redisClient.sendCommand(args) }), keyGenerator: (req) => { // Präziser Key: API-Key > Authenticated User > IP return req.headers['x-api-key'] as string || req.user?.id || req.ip; }, handler: (req, res) => { res.status(429).json({ error: 'Too Many Requests', message: 'Rate limit überschritten. Bitte warte und versuche es erneut.', retryAfter: res.getHeader('Retry-After'), limit: res.getHeader('RateLimit-Limit'), reset: res.getHeader('RateLimit-Reset') }); } }); // Strengerer Limiter für Auth-Endpunkte (Brute-Force-Schutz) const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 Stunde max: 10, // Nur 10 Login-Versuche standardHeaders: true, legacyHeaders: false, store: new RedisStore({ sendCommand: (...args: string[]) => redisClient.sendCommand(args) }), skipSuccessfulRequests: true // Erfolgreiche Logins nicht zählen }); // KI-API Limiter (Claude Code Integration) const aiLimiter = rateLimit({ windowMs: 60 * 1000, // 1 Minute max: 20, // 20 AI-Requests pro Minute standardHeaders: true, legacyHeaders: false, store: new RedisStore({ sendCommand: (...args: string[]) => redisClient.sendCommand(args) }), keyGenerator: (req) => `ai:${req.headers['x-api-key'] || req.user?.id || req.ip}` }); // Middleware anwenden app.use('/api/', apiLimiter); app.use('/api/auth/', authLimiter); app.use('/api/ai/', aiLimiter);

Retry-After Header korrekt setzen

RFC 7231-konforme Retry-After Middleware

HTTP HEADERS
// Custom Middleware für präzise Retry-After Headers function retryAfterMiddleware( req: Request, res: Response, next: NextFunction ): void { const originalJson = res.json.bind(res); res.json = (body: unknown) => { if (res.statusCode === 429) { // RFC 7231: Retry-After als Sekunden-Delta const retryAfterMs = res.getHeader('Retry-After') as number; if (retryAfterMs) { res.setHeader('Retry-After', Math.ceil(retryAfterMs / 1000)); } // X-RateLimit-Reset als Unix-Timestamp (Epoch Sekunden) res.setHeader( 'X-RateLimit-Reset', Math.ceil((Date.now() + (retryAfterMs || 60000)) / 1000) ); } return originalJson(body); }; next(); } app.use(retryAfterMiddleware);

Wichtig: Ohne standardHeaders: true werden die standardisierten RateLimit-* Headers (RFC 6585 / IETF Draft) nicht gesendet. Clients wie das Claude Code SDK nutzen diese Headers für automatisches Retry-Backoff. Immer aktivieren!

5. Distributed Rate Limiting

Wenn dein Service horizontal skaliert — mehrere Pods in Kubernetes, mehrere Lambda-Instanzen in AWS — reicht ein einzelner Redis-Node nicht mehr. Hier kommen Redis Cluster, Redis Sentinel und alternative Lösungen ins Spiel.

Redis Cluster für Hochverfügbarkeit

Redis Cluster Konfiguration

CLUSTER
import { Cluster } from 'ioredis'; // Redis Cluster mit 3 Master-Nodes (+ je 1 Replica) const cluster = new Cluster([ { host: 'redis-node-1', port: 6379 }, { host: 'redis-node-2', port: 6379 }, { host: 'redis-node-3', port: 6379 } ], { redisOptions: { password: process.env.REDIS_PASSWORD, tls: process.env.NODE_ENV === 'production' ? {} : undefined }, clusterRetryStrategy: (times) => { if (times > 5) return null; // Aufgeben nach 5 Versuchen return Math.min(times * 100, 2000); // Exponentielles Backoff } }); // WICHTIG: Bei Cluster ALLE Keys für denselben User // müssen auf demselben Slot landen → Hash Tags verwenden function makeClusterKey(userId: string, windowMs: number): string { // {userId} = Hash Tag → garantiert selber Cluster-Slot return `rl:{${userId}}:${windowMs}`; } // Lua-Script auf Cluster (EVALSHA funktioniert mit Hash Tags) const result = await cluster.evalsha( scriptSha, 1, makeClusterKey('user:abc', 60000), Date.now().toString(), '60000', '100', nanoid() );

Race Conditions in Distributed Systemen

Selbst mit Redis können Race Conditions auftreten, wenn das Lua-Script auf mehreren Nodes läuft. Das klassische Problem: Zwei Pods fragen gleichzeitig an, beide sehen "99/100", beide erlauben — effektiv 101 Requests.

Redlock-Algorithmus für kritische Limits

REDLOCK
import Redlock from 'redlock'; // Redlock mit 3 unabhängigen Redis-Instanzen const redlock = new Redlock( [redisA, redisB, redisC], { driftFactor: 0.01, retryCount: 3, retryDelay: 200, // 200ms zwischen Versuchen retryJitter: 50 // Jitter verhindert Thundering Herd } ); async function criticalRateCheck(userId: string): Promise<boolean> { const lockKey = `lock:rl:${userId}`; await using lock = await redlock.acquire([lockKey], 500); // Kritischer Abschnitt — atomar unter Lock const result = await rateLimiter.check(userId, 100, 60000); return result.allowed; // Lock wird automatisch freigegeben (using-Syntax) } // Hinweis: Redlock nur für sehr kritische Limits nutzen. // Lua-Script allein reicht für 99.9% der Fälle!

Fazit Distributed: Für die meisten APIs (bis ~10.000 req/s) reicht ein einzelner Redis-Node mit Lua-Script. Redis Cluster ab ~50.000 req/s oder bei Hochverfügbarkeitsanforderungen. Redlock nur für Limits mit absoluter Präzision (Zahlungstransaktionen, Quotas mit Abrechnungsrelevanz).

6. API Gateway Integration (Kong, AWS, Nginx)

Wer Rate Limiting nicht im Applikationscode implementieren möchte, kann auf API-Gateway-Lösungen setzen. Das verlagert die Limiterkennung vor den Applikationsserver und entlastet die Backend-Infrastruktur vollständig.

Kong Rate Limiting Plugin

Kong Rate Limiting Advanced — Konfiguration

KONG
# Kong Admin API: Plugin auf Service anwenden # Benötigt: Kong Gateway + rate-limiting-advanced Plugin curl -X POST http://localhost:8001/services/claude-api-service/plugins \ --data "name=rate-limiting-advanced" \ --data "config.limit=100" \ --data "config.window_size=60" \ --data "config.sync_rate=0" \ --data "config.strategy=redis" \ --data "config.redis.host=redis-cluster" \ --data "config.redis.port=6379" \ --data "config.redis.password=\${REDIS_PASSWORD}" \ --data "config.namespace=claude-api" \ --data "config.hide_client_headers=false" # Alternativ: Declarative Config (deck / kong.yaml) --- services: - name: claude-api-service url: http://backend:3000 plugins: - name: rate-limiting-advanced config: limit: - 100 # 100 pro Minute - 2000 # 2000 pro Stunde window_size: - 60 - 3600 strategy: redis redis: host: redis-cluster port: 6379 identifier: consumer # consumer | ip | credential window_type: sliding # sliding | fixed

AWS API Gateway Usage Plans

AWS API Gateway — Usage Plans & API Keys

AWS
// AWS CDK: API Gateway mit Rate Limiting import * as apigateway from 'aws-cdk-lib/aws-apigateway'; const api = new apigateway.RestApi(this, 'ClaudeApi', { restApiName: 'claude-code-api', deployOptions: { // Stage-Level Default Throttling throttlingRateLimit: 1000, // 1000 req/s Steady-State throttlingBurstLimit: 2000 // 2000 req/s Burst } }); // Usage Plan für Tier-basiertes Limiting const starterPlan = new apigateway.UsagePlan(this, 'StarterPlan', { name: 'Starter', throttle: { rateLimit: 10, // 10 req/s burstLimit: 20 // 20 req Burst }, quota: { limit: 1000, // 1000 req/Monat period: apigateway.Period.MONTH } }); const proPlan = new apigateway.UsagePlan(this, 'ProPlan', { name: 'Professional', throttle: { rateLimit: 100, burstLimit: 200 }, quota: { limit: 50000, period: apigateway.Period.MONTH } }); // API Keys für Authentifizierung const apiKey = new apigateway.ApiKey(this, 'CustomerKey', { apiKeyName: 'customer-key-prod', enabled: true }); starterPlan.addApiKey(apiKey);

Nginx limit_req — Performantes Gateway-Rate-Limiting

Nginx Rate Limiting Konfiguration

NGINX
# /etc/nginx/nginx.conf http { # Rate Limit Zone definieren # key: $binary_remote_addr = kompakte IP (4 Bytes IPv4) # zone=api:10m = 10MB Shared Memory (~160.000 IPs) # rate=100r/m = 100 Requests pro Minute limit_req_zone $binary_remote_addr zone=api_general:10m rate=100r/m; limit_req_zone $binary_remote_addr zone=api_auth:5m rate=10r/m; limit_req_zone $http_x_api_key zone=api_by_key:20m rate=1000r/m; server { listen 443 ssl http2; server_name api.agentic-movers.com; location /api/ { # burst=20: 20 Requests "puffern" (danach 503) # nodelay: gebufferte Requests SOFORT verarbeiten limit_req zone=api_general burst=20 nodelay; limit_req_status 429; # HTTP 429 statt 503 # Retry-After Header setzen add_header Retry-After 60 always; add_header X-RateLimit-Limit 100 always; proxy_pass http://backend_cluster; } location /api/auth/ { limit_req zone=api_auth burst=5; limit_req_status 429; proxy_pass http://backend_cluster; } location /api/ai/ { # API-Key basiertes Limiting (wenn X-API-Key gesetzt) limit_req zone=api_by_key burst=50 nodelay; limit_req_status 429; proxy_pass http://backend_cluster; } } }

Welche Lösung passt wann?

Lösung Skala Flexibilität Aufwand Empfohlen für
express-rate-limit Mittel Hoch Niedrig Startups, Self-hosted
Redis Lua Hoch Sehr hoch Mittel Produktion, Multi-Tenant
Kong Sehr hoch Mittel Mittel Enterprise, Kubernetes
AWS API Gateway Unbegrenzt Mittel Hoch AWS-native, Serverless
Nginx Sehr hoch Niedrig Niedrig Einfache Szenarien

Fazit: Die richtige Rate-Limiting-Strategie für 2026

Rate Limiting ist keine "Nice-to-have"-Funktion — es ist ein fundamentaler Sicherheits- und Stabilitätsmechanismus für jede produktive API. Gerade bei KI-APIs wie Claude Code, wo ein einzelner unkontrollierter Request erhebliche Kosten verursachen kann, ist ein robustes Rate Limiting unverzichtbar.

Unsere Empfehlung für 2026:

Kombiniere immer Gateway-Level-Schutz (Nginx/Kong) mit Applikations-Level-Limits (Redis). Das "Defense in Depth"-Prinzip sorgt dafür, dass selbst bei Bypass-Versuchen ein zweiter Schutzwall greift.

Starte deinen KI-Agenten mit solidem Rate Limiting

SpockyMagicAI bietet KI-Automatisierungs-Infrastruktur mit eingebautem Rate Limiting, Redis-Backend und Claude Code Integration — einsatzbereit in Minuten.

Kostenlos testen →