API-Ratenbegrenzung: Muster und Best Practices
Jeder öffentliche API-Endpunkt ist ein Ziel – Bots, Scraper und böswillige Akteure werden Ihren Server attackieren, sobald Sie ihn in Betrieb nehmen. Ohne Ratenbegrenzung kann ein einziger sich schlecht verhaltender Client Ihre Datenbankverbindungen erschöpfen, Ihre Cloud-Rechnung in die Höhe treiben und den Dienst für jeden legitimen Benutzer lahm legen. Die Ratenbegrenzung ist nicht optional; Es ist die erste Verteidigungslinie für jede Produktions-API.
In diesem Leitfaden werden die vier wichtigsten Algorithmen zur Ratenbegrenzung, ihre Kompromisse und die korrekte Implementierung in NestJS mit Redis erläutert. Sie erhalten Konfigurationen zum Kopieren und Einfügen für gängige Szenarien und ein mentales Modell für die Auswahl der richtigen Strategie pro Endpunkt.
Wichtige Erkenntnisse
- Der Token-Bucket ermöglicht ein kontrolliertes Bursting, während Zähler mit festem Fenster am kostengünstigsten zu implementieren sind
- Das Sliding-Window-Protokoll ist das genaueste, aber speicherintensivste; Schiebefenstertheke ist die beste Balance – Redis ist der einzig richtige Sicherungsspeicher, wenn Sie mehrere API-Replikate ausführen – NestJS
@nestjs/throttlerunterstützt benutzerdefinierte Speicheradapter – Tauschen Sie Redis mit einer Konfigurationsänderung aus – Gibt immer die HeaderRetry-After,X-RateLimit-Limit,X-RateLimit-RemainingundX-RateLimit-Resetzurück- Grenzen nach Endpunktempfindlichkeit unterscheiden: Authentifizierung (5/Min.) vs. Lese-APIs (1000/Min.) – Verwenden Sie IP-basierte Limits für anonymen Datenverkehr und benutzerbasierte Limits für authentifizierte Anfragen – Lassen Sie Anfragen niemals stillschweigend fallen – geben Sie
429 Too Many Requestsimmer mit einer hilfreichen Nachricht zurück
Die vier Kernalgorithmen
Fensterzähler behoben
Der einfachste Ansatz: Anfragen in einem festen Zeitfenster zählen, an der Grenze zurücksetzen.
// Fixed window: 100 requests per minute
// Window resets at :00, :01, :02 ...
const windowKey = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(windowKey);
await redis.expire(windowKey, 60);
if (count > 100) {
throw new TooManyRequestsException();
}
Schwäche: Der Grenz-Exploit. Ein Client kann um 12:00:59 Uhr 100 Anfragen und um 12:01:00 weitere 100 Anfragen senden – effektiv 200 Anfragen in zwei Sekunden. Für die meisten APIs ist dies akzeptabel. Für Authentifizierungsendpunkte ist dies nicht der Fall.
Schiebefensterprotokoll
Speichern Sie den Zeitstempel jeder Anfrage. Zählen Sie bei jeder Anfrage die Zeitstempel im letzten Fenster.
const now = Date.now();
const windowStart = now - 60_000; // 60 seconds ago
// Remove old entries, add current
await redis.zremrangebyscore(key, 0, windowStart);
await redis.zadd(key, now, now.toString());
const count = await redis.zcard(key);
await redis.expire(key, 60);
if (count > 100) {
throw new TooManyRequestsException();
}
Kompromiss: Vollkommen genau, speichert aber O(n) Einträge pro Benutzer, wobei n die Anzahl der Anfragen ist. Bei 1.000 RPS bei 10.000 Benutzern steigt Ihr Redis-Speicher schnell an. Verwendung für Endpunkte mit geringem Volumen und hoher Sicherheit, z. B. zum Zurücksetzen von Passwörtern.
Schiebefenstertheke
Ungefähres Schiebefenster mit zwei festen Fenstern – keine Speicherexplosion.
const now = Date.now();
const currentWindow = Math.floor(now / 60000);
const previousWindow = currentWindow - 1;
const windowProgress = (now % 60000) / 60000; // 0.0 to 1.0
const [current, previous] = await redis.mget(
`rl:${userId}:${currentWindow}`,
`rl:${userId}:${previousWindow}`
);
const estimated =
(parseInt(previous ?? '0') * (1 - windowProgress)) +
parseInt(current ?? '0');
if (estimated >= 100) {
throw new TooManyRequestsException();
}
await redis.incr(`rl:${userId}:${currentWindow}`);
await redis.expire(`rl:${userId}:${currentWindow}`, 120);
Dies ist der Algorithmus, den Cloudflare verwendet. Es glättet die Grenzspitze mit minimalem Overhead – zwei Redis-Schlüssel pro Benutzer und Fenster.
Token-Bucket
Der Goldstandard für das Zulassen von Bursts bei gleichzeitiger Beibehaltung einer langfristigen Rate. Jeder Benutzer verfügt über einen Eimer, der sich konstant füllt. Anfragen verbrauchen Token.
async function consumeToken(
redis: Redis,
userId: string,
ratePerSec: number,
capacity: number
): Promise<boolean> {
const now = Date.now() / 1000;
const key = `bucket:${userId}`;
const values = await redis.hmget(key, 'tokens', 'lastRefill');
const currentTokens = parseFloat(values[0] ?? String(capacity));
const lastRefillTime = parseFloat(values[1] ?? String(now));
// Refill tokens based on elapsed time
const elapsed = now - lastRefillTime;
const refilled = Math.min(capacity, currentTokens + elapsed * ratePerSec);
if (refilled < 1) {
return false; // No tokens available
}
await redis.hset(key, 'tokens', String(refilled - 1), 'lastRefill', String(now));
await redis.expire(key, Math.ceil(capacity / ratePerSec) + 60);
return true;
}
Der Token-Bucket ist ideal für APIs, die kurze Bursts (das gleichzeitige Hochladen von 10 Dateien) zulassen und gleichzeitig anhaltenden Missbrauch verhindern müssen.
NestJS Throttler-Konfiguration
@nestjs/throttler v5 wird mit einem Redis-Speicheradapter geliefert. Hier ist ein produktionsbereites Setup:
pnpm add @nestjs/throttler @nestjs/throttler-storage-redis ioredis
// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import { APP_GUARD } from '@nestjs/core';
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
throttlers: [
{ name: 'short', ttl: 1000, limit: 5 }, // 5 req/sec burst
{ name: 'medium', ttl: 60000, limit: 300 }, // 300 req/min
{ name: 'long', ttl: 3600000, limit: 5000 }, // 5000 req/hr
],
storage: new ThrottlerStorageRedisService({
host: config.get('REDIS_HOST'),
port: config.get('REDIS_PORT'),
}),
}),
}),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
Überschreiben Sie die Grenzwerte pro Controller oder Route:
@Controller('auth')
export class AuthController {
// Authentication: very strict — 5 attempts per minute
@Post('login')
@Throttle({ medium: { ttl: 60000, limit: 5 } })
async login(@Body() dto: LoginDto) { /* ... */ }
// Refresh: moderate — 30 per minute
@Post('refresh')
@Throttle({ medium: { ttl: 60000, limit: 30 } })
async refresh(@Body() dto: RefreshDto) { /* ... */ }
// Skip throttling on the exchange endpoint (protected by one-time code TTL)
@Post('exchange')
@SkipThrottle()
async exchange(@Body() dto: ExchangeDto) { /* ... */ }
}
Benutzerdefinierte Schlüsselgeneratoren
Standardmäßig verwendet der NestJS-Drosseler die Client-IP. In der Produktion hinter Nginx/Cloudflare benötigen Sie X-Real-IP oder CF-Connecting-IP.
// throttler-behind-proxy.guard.ts
import { ThrottlerGuard } from '@nestjs/throttler';
import { Injectable, ExecutionContext } from '@nestjs/common';
@Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
// Authenticated user — use userId for accurate per-user limits
if (req.user?.sub) {
return `user:${req.user.sub}`;
}
// Anonymous — use real IP from Cloudflare header
return (
req.headers['cf-connecting-ip'] ||
req.headers['x-real-ip'] ||
(req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
req.ip
);
}
protected async throwThrottlingException(
context: ExecutionContext,
throttlerLimitDetail: ThrottlerLimitDetail
): Promise<void> {
const response = context.switchToHttp().getResponse();
response.header(
'Retry-After',
Math.ceil(throttlerLimitDetail.ttl / 1000)
);
await super.throwThrottlingException(context, throttlerLimitDetail);
}
}
Antwortheader
RFC 6585 und RateLimit-Header-Entwürfe teilen den Clients genau mit, wann sie es erneut versuchen sollen:
// rate-limit.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class RateLimitHeadersInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
tap(() => {
const res = context.switchToHttp().getResponse();
const req = context.switchToHttp().getRequest();
// Values injected by ThrottlerGuard after evaluation
if (req.rateLimit) {
res.set({
'X-RateLimit-Limit': req.rateLimit.limit,
'X-RateLimit-Remaining': Math.max(
0,
req.rateLimit.limit - req.rateLimit.current
),
'X-RateLimit-Reset': new Date(
Date.now() + req.rateLimit.ttl
).toISOString(),
'RateLimit-Policy': `${req.rateLimit.limit};w=${Math.ceil(
req.rateLimit.ttl / 1000
)}`,
});
}
})
);
}
}
Endpunktspezifische Strategien
Unterschiedliche Endpunkte erfordern unterschiedliche Grenzwerte. Hier ist eine Referenztabelle für gängige Muster:
| Endpunkttyp | Algorithmus | Grenze | Fenster |
|---|---|---|---|
| Login / Passwort zurücksetzen | Schiebefenster-Log | 5 | 15 Minuten |
| OTP / 2FA-Überprüfung | Festes Fenster | 3 | 10 Minuten |
| Öffentliche Lese-API | Token-Eimer | 1000 Burst, 100/s Füllung | — |
| Mutations-API (authentifiziert) | Schiebefenstertheke | 300 | 1 Minute |
| Webhook-Aufnahme | Festes Fenster | 10.000 | 1 Minute |
| Datei-Upload | Token-Eimer | 10 Burst, 1/s Füllung | — |
| KI/LLM-Endpunkte | Festes Fenster | 20 | 1 Minute |
| Suche (anonym) | Festes Fenster | 30 | 1 Minute |
Atomic Lua-Skripte für verteilte Sicherheit
Wenn Sie über mehrere API-Replikate verfügen, können Race-Bedingungen in Inkrementprüfungssequenzen Bursts über dem Grenzwert ermöglichen. Verwenden Sie ein über redis.defineCommand geladenes Lua-Skript, um die Prüfung und Inkrementierung atomar zu machen:
// rate-limit.service.ts
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RateLimitService {
constructor(private readonly redis: Redis) {
// Define atomic increment+check as a custom Redis command
this.redis.defineCommand('rateLimitCheck', {
numberOfKeys: 1,
lua: `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttlMs = tonumber(ARGV[2])
local count = redis.call('INCR', key)
if count == 1 then
redis.call('PEXPIRE', key, ttlMs)
end
if count > limit then
return {0, redis.call('PTTL', key)}
end
return {1, -1}
`,
});
}
async isAllowed(
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; retryAfterMs: number }> {
const result = await (this.redis as any).rateLimitCheck(
key, limit, windowMs
) as [number, number];
return {
allowed: result[0] === 1,
retryAfterMs: result[1] > 0 ? result[1] : 0,
};
}
}
Graceful Degradation- und Bypass-Strategien
Durch die Ratenbegrenzung sollten interne Gesundheitsprüfungen, Überwachungsagenten oder vertrauenswürdige Partner nicht blockiert werden.
// trusted-bypass.guard.ts
@Injectable()
export class RateLimitWithBypassGuard extends ThrottlerBehindProxyGuard {
private readonly trustedTokens = new Set([
process.env.MONITORING_TOKEN,
process.env.PARTNER_API_TOKEN,
].filter(Boolean));
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
// Internal health checks bypass all rate limits
if (req.path === '/health' || req.path === '/ready') {
return true;
}
// Trusted API tokens bypass
const token = req.headers['x-bypass-token'];
if (token && this.trustedTokens.has(token)) {
return true;
}
return super.canActivate(context);
}
}
Für eine progressive Ratenbegrenzung (Warnung vor harter Blockierung) geben Sie 429 mit einem Retry-After-Header erst zurück, nachdem 80 % des Grenzwerts überschritten wurden:
// In your custom guard, after counting requests:
if (count > limit * 0.9 && count <= limit) {
response.set('X-RateLimit-Warning', 'approaching limit');
}
if (count > limit) {
response.set('Retry-After', retryAfterSeconds.toString());
throw new HttpException('Rate limit exceeded', 429);
}
Ratenbegrenzung testen
// rate-limit.spec.ts
describe('Rate Limiting', () => {
it('should block after limit exceeded', async () => {
const app = moduleRef.createNestApplication();
await app.init();
// Hit the endpoint 5 times (limit for login)
for (let i = 0; i < 5; i++) {
await request(app.getHttpServer())
.post('/auth/login')
.send({ email: '[email protected]', password: 'wrongpassword' })
.expect((res) => expect(res.status).toBeLessThan(429));
}
// 6th request should be blocked
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: '[email protected]', password: 'wrongpassword' });
expect(response.status).toBe(429);
expect(response.headers['retry-after']).toBeDefined();
expect(response.body.message).toContain('rate limit');
});
});
Überwachung und Alarmierung
Ratenbegrenzungsereignisse sind wertvolle Signale. Melden Sie sie auf Ihrer Observability-Plattform an:
@Injectable()
export class RateLimitMetricsService {
async recordRateLimitHit(userId: string, endpoint: string, ip: string) {
await this.metricsService.increment('rate_limit.hits', {
endpoint,
user_type: userId ? 'authenticated' : 'anonymous',
});
// Alert on sustained attacks (>100 hits in 1 min from same IP)
const alertKey = `rl_alert:${ip}`;
const recentHits = await this.redis.incr(alertKey);
if (recentHits === 1) {
await this.redis.expire(alertKey, 60);
}
if (recentHits === 100) {
await this.alertService.send({
severity: 'high',
message: `Rate limit attack detected from IP ${ip}`,
endpoint,
});
}
}
}
Dashboard-Tracking erstellen: – Ratenlimit-Treffer pro Endpunkt (S. 95, S. 99)
- Top-IPs/Benutzer stoßen an Grenzen – Prozentsatz der blockierten im Vergleich zu bearbeiteten Anfragen – Wiederholungsversuche nach Ablauf, um falsch konfigurierte Clients zu erkennen
Häufig gestellte Fragen
Sollte ich die Rate nach IP oder nach Benutzer-ID begrenzen?
Benutzen Sie beides. Für nicht authentifizierte Endpunkte ist IP die einzige verfügbare Kennung. Bevorzugen Sie bei authentifizierten Endpunkten immer die Benutzer-ID – sie ist genauer und verhindert, dass eine gemeinsame IP (wie ein Unternehmens-NAT) alle Mitarbeiter blockiert. Implementieren Sie eine zweistufige Prüfung: IP-Limit auf Nginx-Ebene und Benutzer-ID-Limit auf Anwendungsebene.
Was ist der richtige HTTP-Statuscode für die Ratenbegrenzung?
Immer 429 Too Many Requests gemäß RFC 6585. Verwenden Sie niemals 503 Service Unavailable (impliziert einen Infrastrukturfehler) oder 403 Forbidden (impliziert einen Autorisierungsfehler). Fügen Sie Retry-After in Sekundenschnelle als Header ein, damit Clients wissen, wann sie es erneut versuchen müssen.
Wie gehe ich mit der Ratenbegrenzung hinter Cloudflare oder einem Load Balancer um?
Konfigurieren Sie Ihren Proxy so, dass er X-Real-IP oder CF-Connecting-IP einstellt und nur dem IP-Bereich Ihres Proxys vertraut. In Nginx: set_real_ip_from 103.21.244.0/22; real_ip_header CF-Connecting-IP;. Legen Sie in NestJS app.set('trust proxy', 1) fest und lesen Sie req.ip, den NestJS aus dem Header des vertrauenswürdigen Proxys auflöst.
Welche Redis-Datenstruktur eignet sich am besten für die Ratenbegrenzung?
Für feste/gleitende Fensterzähler verwenden Sie INCR + EXPIRE für einen Zeichenfolgenschlüssel – O(1) pro Anfrage. Verwenden Sie für das Schiebefensterprotokoll einen sortierten Satz (ZADD, ZREMRANGEBYSCORE, ZCARD) – O(log n). Verwenden Sie für den Token-Bucket einen Hash (HSET, HGET) – O(1). Lua-Skripte machen alle Operationen über alle drei Muster hinweg atomar.
Wie soll ich mit Ratenbeschränkungen für Webhooks von vertrauenswürdigen Anbietern umgehen?
Stripe, GitHub und ähnliche Anbieter senden Webhooks aus bekannten IP-Bereichen. Führen Sie eine Zulassungsliste ihrer CIDR-Bereiche und umgehen Sie die Ratenbegrenzung für diese IPs auf Ihrem Webhook-Aufnahmeendpunkt. Überprüfen Sie zuerst die Webhook-Signatur – die Signaturüberprüfung ist dort Ihre eigentliche Sicherheitsebene und nicht die Ratenbegrenzung.
Kann ich stattdessen eine Ratenbegrenzung auf Nginx-Ebene implementieren?
Ja, und das sollten Sie für einen grundlegenden DDoS-Schutz tun. Verwenden Sie limit_req_zone in Nginx für grobe IP-basierte Grenzwerte (1000 Anforderungen/Min.). Legen Sie eine Ratenbegrenzung auf Anwendungsebene darüber, um eine detaillierte Kontrolle pro Benutzer und Endpunkt zu ermöglichen. Die beiden Schichten ergänzen sich: Nginx bewältigt Volumenangriffe kostengünstig, ohne Ihre Anwendung zu beeinträchtigen, und NestJS bewältigt nuancierte Grenzen der Geschäftslogik.
Nächste Schritte
Der Aufbau einer Produktions-API ohne starke Ratenbegrenzung ist, als ob Sie Ihre Haustür unverschlossen lassen. Die Muster in diesem Leitfaden – Token-Bucket für stoßartigen Datenverkehr, Sliding-Window-Counter für reibungslose Durchsetzung, Redis-gestützter verteilter Speicher und richtige 429-Antworten – bilden das Rückgrat einer sicheren, skalierbaren API.
ECOSIRE erstellt vom ersten Tag an NestJS-Backends der Enterprise-Klasse mit Ratenbegrenzung, Redis-Caching und vollständiger Observability. Wenn Sie eine neue API einführen oder eine bestehende härten, entdecken Sie unsere Backend-Engineering-Services, um zu erfahren, wie wir Ihre Bereitstellung beschleunigen können.
Geschrieben von
ECOSIRE Research and Development Team
Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.
Verwandte Artikel
Cybersecurity Trends 2026-2027: Zero Trust, AI Threats, and Defense
The definitive guide to cybersecurity trends for 2026-2027—AI-powered attacks, zero trust implementation, supply chain security, and building resilient security programs.
Financial Services ERP Implementation: Regulatory and Security Requirements
A practitioner's guide to implementing ERP in regulated financial services firms, covering security controls, compliance validation, data governance, and phased rollout.
Government ERP Implementation: Security Clearance and Compliance
A comprehensive guide to implementing ERP in government agencies, covering FedRAMP compliance, security clearance requirements, change management, and phased deployment.