API Rate Limiting: Patterns and Best Practices

Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.

E
ECOSIRE Research and Development Team
|19. März 202610 Min. Lesezeit2.2k Wörter|

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/throttler unterstützt benutzerdefinierte Speicheradapter – Tauschen Sie Redis mit einer Konfigurationsänderung aus – Gibt immer die Header Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset zurü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 Requests immer 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:

EndpunkttypAlgorithmusGrenzeFenster
Login / Passwort zurücksetzenSchiebefenster-Log515 Minuten
OTP / 2FA-ÜberprüfungFestes Fenster310 Minuten
Öffentliche Lese-APIToken-Eimer1000 Burst, 100/s Füllung
Mutations-API (authentifiziert)Schiebefenstertheke3001 Minute
Webhook-AufnahmeFestes Fenster10.0001 Minute
Datei-UploadToken-Eimer10 Burst, 1/s Füllung
KI/LLM-EndpunkteFestes Fenster201 Minute
Suche (anonym)Festes Fenster301 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.

E

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.

Chatten Sie auf WhatsApp