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 Mart 202610 dk okuma2.2k Kelime|

API Hız Sınırlaması: Kalıplar ve En İyi Uygulamalar

Her genel API uç noktası bir hedeftir; botlar, kazıyıcılar ve kötü aktörler, yayına girdiğiniz anda sunucunuza zarar verecektir. Hız sınırlaması olmaksızın, hatalı davranan tek bir istemci, veritabanı bağlantılarınızı tüketebilir, bulut faturanızı artırabilir ve her meşru kullanıcı için hizmeti devre dışı bırakabilir. Hız sınırlaması isteğe bağlı değildir; herhangi bir üretim API'sinin ilk savunma hattıdır.

Bu kılavuz dört ana hız sınırlayıcı algoritmayı, bunların değiş tokuşlarını ve bunların Redis ile NestJS'de doğru şekilde nasıl uygulanacağını açıklamaktadır. Ortak senaryolar için kopyala-yapıştır yapılandırmaları ve uç nokta başına doğru stratejiyi seçmeye yönelik zihinsel bir modelle ayrılacaksınız.

Önemli Çıkarımlar

  • Jeton kovası kontrollü patlamaya izin verirken, sabit pencere sayaçları uygulaması en ucuz olanlardır
  • Kayan pencere günlüğü en doğru olanıdır ancak yoğun bellek harcar; sürgülü pencere sayacı en iyi dengedir
  • Birden fazla API replikası çalıştırdığınızda Redis tek doğru yedekleme deposudur
  • NestJS @nestjs/throttler özel depolama adaptörlerini destekler — tek bir yapılandırma değişikliğiyle Redis'te takas yapın
  • Her zaman Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining ve X-RateLimit-Reset başlıklarını döndür
  • Sınırları uç nokta hassasiyetine göre farklılaştırın: kimlik doğrulama (5/dak) ve okuma API'leri (1000/dak)
  • Anonim trafik için IP tabanlı sınırları ve kimliği doğrulanmış istekler için kullanıcı tabanlı sınırları kullanın
  • İstekleri asla sessizce bırakmayın — her zaman yararlı bir mesajla 429 Too Many Requests değerini döndürün

Dört Çekirdekli Algoritma

Sabit Pencere Sayacı

En basit yaklaşım: istekleri sabit bir zaman penceresinde sayın, sınırda sıfırlayın.

// 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();
}

Zayıflık: Sınır istismarı. Bir müşteri 100 isteği 12:00:59'da ve 100 isteği 12:01:00'de gönderebilir; yani iki saniyede 200 istek. Çoğu API için bu kabul edilebilir. Kimlik doğrulama uç noktaları için bu geçerli değildir.

Kayan Pencere Günlüğü

Her istek zaman damgasını saklayın. Her istekte son penceredeki zaman damgalarını sayın.

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();
}

Değiştirme: Tamamen doğrudur ancak kullanıcı başına O(n) girişi saklar; burada n, istek sayısıdır. 10.000 kullanıcıda 1.000 RPS ile Redis belleğiniz hızla artıyor. Parola sıfırlama gibi düşük hacimli, yüksek güvenlikli uç noktalar için kullanın.

Sürgülü Pencere Sayacı

İki sabit pencere kullanan yaklaşık kayan pencere — bellek patlaması yok.

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);

Bu Cloudflare'in kullandığı algoritmadır. Pencere başına kullanıcı başına iki Redis anahtarıyla sınır artışını minimum düzeyde ek yük ile yumuşatır.

Jeton Kovası

Uzun vadeli bir oranı korurken patlamalara izin veren altın standart. Her kullanıcının sabit bir oranda dolan bir kovası vardır. İstekler jeton tüketir.

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;
}

Token kovası, sürekli kötüye kullanımı önlerken kısa aralıklarla (aynı anda 10 dosya yükleme) izin vermesi gereken API'ler için idealdir.


NestJS Kısıtlayıcı Yapılandırması

@nestjs/throttler v5, Redis depolama bağdaştırıcısıyla birlikte gelir. İşte üretime hazır bir kurulum:

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 {}

Denetleyici veya rota başına geçersiz kılma sınırları:

@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) { /* ... */ }
}

Özel Anahtar Oluşturucular

NestJS kısıtlayıcı varsayılan olarak istemci IP'sini kullanır. Nginx/Cloudflare'in arkasındaki üretimde X-Real-IP veya CF-Connecting-IP'ye ihtiyacınız var.

// 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);
  }
}

Yanıt Başlıkları

RFC 6585 ve taslak RateLimit başlıkları, müşterilere tam olarak ne zaman yeniden denemeleri gerektiğini söyler:

// 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
            )}`,
          });
        }
      })
    );
  }
}

Uç Noktaya Özel Stratejiler

Farklı uç noktalar farklı limitleri garanti eder. Yaygın modeller için bir referans tablosu aşağıda verilmiştir:

Uç Nokta TürüAlgoritmaSınırıPencere
Giriş / şifre sıfırlamaSürgülü pencere günlüğü515 dakika
OTP / 2FA doğrulamasıSabit pencere310 dakika
Herkese açık okuma API'siJeton kovası1000 patlama, 100/s dolum
Mutasyon API'si (kimliği doğrulanmış)Sürgülü pencere sayacı3001 dakika
Web kancası alımıSabit pencere10.0001 dakika
Dosya yüklemeJeton kovası10 patlama, 1/s dolum
AI / LLM uç noktalarıSabit pencere201 dakika
Ara (anonim)Sabit pencere301 dakika

Dağıtılmış Güvenlik için Atomik Lua Komut Dosyaları

Birden fazla API kopyanız olduğunda, artış kontrolü dizilerindeki yarış koşulları, sınırın üzerinde artışlara izin verebilir. Atomik kontrol ve artış işlemini gerçekleştirmek için redis.defineCommand yoluyla yüklenen bir Lua betiğini kullanın:

// 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,
    };
  }
}

Zarif Bozulma ve Baypas Stratejileri

Hız sınırlaması dahili durum kontrollerini, izleme aracılarını veya güvenilir iş ortaklarını engellememelidir.

// 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);
  }
}

Aşamalı hız sınırlaması için (sert bloklamadan önce uyar), yalnızca sınırın %80'ini geçtikten sonra 429 başlığını Retry-After başlığıyla döndürün:

// 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);
}

Test Hızı Sınırlaması

// 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');
  });
});

İzleme ve Uyarı

Hız sınırı olayları değerli sinyallerdir. Bunları gözlemlenebilirlik platformunuza kaydedin:

@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,
      });
    }
  }
}

Kontrol panelleri takibi oluşturun:

  • Uç nokta başına oran sınırı isabetleri (p95, p99)
  • Sınırlara ulaşan en iyi IP'ler/kullanıcılar
  • Engellenen ve sunulan isteklerin yüzdesi
  • Yanlış yapılandırılmış istemcileri tespit etmek için tekrar deneme süreleri

Sıkça Sorulan Sorular

Sınırı IP'ye göre mi yoksa kullanıcı kimliğine göre mi derecelendirmeliyim?

Her ikisini de kullanın. Kimliği doğrulanmamış uç noktalar için mevcut tek tanımlayıcı IP'dir. Kimliği doğrulanmış uç noktalar için her zaman kullanıcı kimliğini tercih edin; bu daha doğrudur ve paylaşılan bir IP'nin (kurumsal NAT gibi) tüm çalışanları engellemesini önler. İki aşamalı bir kontrol uygulayın: Nginx düzeyinde IP sınırı ve uygulama düzeyinde kullanıcı kimliği sınırı.

Hız sınırlaması için doğru HTTP durum kodu nedir?

RFC 6585'e göre her zaman 429 Too Many Requests. Asla 503 Service Unavailable (altyapı arızasını belirtir) veya 403 Forbidden (yetkilendirme arızasını belirtir) kullanmayın. İstemcilerin ne zaman yeniden deneyeceklerini bilmeleri için Retry-After'ü saniyeler içinde başlık olarak ekleyin.

Cloudflare veya yük dengeleyicinin arkasındaki hız sınırlamasını nasıl hallederim?

Proxy'nizi X-Real-IP veya CF-Connecting-IP ayarlayacak şekilde yapılandırın ve yalnızca proxy'nizin IP aralığına güvenin. Nginx'te: set_real_ip_from 103.21.244.0/22; real_ip_header CF-Connecting-IP;. NestJS'de app.set('trust proxy', 1) değerini ayarlayın ve NestJS'nin güvenilir proxy başlığından çözdüğü req.ip değerini okuyun.

Hız sınırlaması için en iyi Redis veri yapısı hangisidir?

Sabit/kayan pencere sayaçları için, istek başına O(1) dize anahtarında INCR + EXPIRE kullanın. Kayan pencere günlüğü için sıralanmış bir küme kullanın (ZADD, ZREMRANGEBYSCORE, ZCARD) — O(log n). Belirteç kümesi için bir karma kullanın (HSET, HGET) — O(1). Lua komut dosyaları, tüm işlemleri üç modelin tamamında atomik hale getirir.

Güvenilir sağlayıcıların web kancalarına ilişkin hız sınırlarını nasıl yönetmeliyim?

Stripe, GitHub ve benzeri sağlayıcılar bilinen IP aralıklarından web kancaları gönderir. Webhook besleme uç noktanızda CIDR aralıklarının izin verilenler listesini tutun ve bu IP'ler için hız sınırlamasını atlayın. Önce webhook imzasını doğrulayın; imza doğrulaması, hız sınırlaması değil, oradaki gerçek güvenlik katmanınızdır.

Bunun yerine Nginx düzeyinde hız sınırlaması uygulayabilir miyim?

Evet ve temel DDoS koruması için bunu yapmalısınız. Kaba IP tabanlı sınırlar (1000 istek/dak) için Nginx'te limit_req_zone kullanın. Kullanıcı başına, uç nokta başına ayrıntılı kontrol için uygulama düzeyinde hız sınırlaması katmanı. İki katman birbirini tamamlar: Nginx, uygulamanıza zarar vermeden toplu saldırıları ucuz bir şekilde yönetir ve NestJS, incelikli iş mantığı sınırlarını yönetir.


Sonraki Adımlar

Sağlam hız sınırlaması olmadan bir üretim API'si oluşturmak, ön kapınızın kilidini açık bırakmak gibidir. Bu kılavuzdaki modeller - yoğun trafik için belirteç kümesi, sorunsuz uygulama için kayan pencere sayacı, Redis destekli dağıtılmış depolama ve uygun 429 yanıtları - güvenli, ölçeklenebilir bir API'nin omurgasını oluşturur.

ECOSIRE, ilk günden itibaren hız sınırlama, Redis önbelleğe alma ve tam gözlemlenebilirlik özelliklerine sahip kurumsal düzeyde NestJS arka uçları oluşturur. Yeni bir API başlatıyorsanız veya mevcut olanı sağlamlaştırıyorsanız, dağıtımınızı nasıl hızlandırabileceğimizi görmek için arka uç mühendislik hizmetlerimizi keşfedin.

E

Yazan

ECOSIRE Research and Development Team

ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.

WhatsApp'ta Sohbet Et