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 मार्च 202611 मिनट पढ़ें2.5k शब्द|

एपीआई दर सीमित करना: पैटर्न और सर्वोत्तम प्रथाएं

प्रत्येक सार्वजनिक एपीआई एंडपॉइंट एक लक्ष्य है - जैसे ही आप लाइव होंगे, बॉट, स्क्रेपर्स और बुरे कलाकार आपके सर्वर पर हमला कर देंगे। दर सीमित किए बिना, एक भी दुर्व्यवहार करने वाला ग्राहक आपके डेटाबेस कनेक्शन को समाप्त कर सकता है, आपके क्लाउड बिल को बढ़ा सकता है, और प्रत्येक वैध उपयोगकर्ता के लिए सेवा बंद कर सकता है। दर सीमित करना वैकल्पिक नहीं है; यह किसी भी उत्पादन एपीआई के लिए रक्षा की पहली पंक्ति है।

यह मार्गदर्शिका चार प्रमुख दर-सीमित एल्गोरिदम, उनके ट्रेड-ऑफ़ और रेडिस के साथ NestJS में उन्हें सही तरीके से लागू करने के तरीके के बारे में बताती है। आप सामान्य परिदृश्यों के लिए कॉपी-पेस्ट कॉन्फ़िगरेशन और प्रत्येक समापन बिंदु पर सही रणनीति चुनने के लिए एक मानसिक मॉडल के साथ निकलेंगे।

मुख्य बातें

  • टोकन बकेट नियंत्रित विस्फोट की अनुमति देता है जबकि फिक्स्ड विंडो काउंटर लागू करने के लिए सबसे सस्ते हैं
  • स्लाइडिंग विंडो लॉग सबसे सटीक लेकिन मेमोरी-गहन है; स्लाइडिंग विंडो काउंटर सबसे अच्छा संतुलन है
  • जब आप एकाधिक एपीआई प्रतिकृतियां चलाते हैं तो रेडिस एकमात्र सही बैकिंग स्टोर है
  • NestJS @nestjs/throttler कस्टम स्टोरेज एडेप्टर का समर्थन करता है - एक कॉन्फ़िगरेशन परिवर्तन के साथ Redis में स्वैप करें
  • हमेशा Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, और X-RateLimit-Reset हेडर लौटाएँ
  • समापन बिंदु संवेदनशीलता के आधार पर सीमाएं अलग करें: प्रमाणीकरण (5/मिनट) बनाम रीड एपीआई (1000/मिनट)
  • अनाम ट्रैफ़िक के लिए आईपी-आधारित सीमा और प्रमाणित अनुरोधों के लिए उपयोगकर्ता-आधारित सीमा का उपयोग करें
  • कभी भी चुपचाप अनुरोध न छोड़ें - हमेशा एक उपयोगी संदेश के साथ 429 Too Many Requests लौटाएं

चार मुख्य एल्गोरिदम

फिक्स्ड विंडो काउंटर

सबसे सरल तरीका: एक निश्चित समय विंडो में अनुरोधों की गणना करें, सीमा पर रीसेट करें।

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

कमजोरी: सीमा शोषण। एक ग्राहक 12:00:59 पर 100 अनुरोध और 12:01:00 पर अन्य 100 अनुरोध भेज सकता है - प्रभावी रूप से दो सेकंड में 200 अनुरोध। अधिकांश एपीआई के लिए यह स्वीकार्य है। प्रमाणीकरण समापन बिंदुओं के लिए, यह नहीं है।

स्लाइडिंग विंडो लॉग

प्रत्येक अनुरोध टाइमस्टैम्प को संग्रहीत करें। प्रत्येक अनुरोध पर, अंतिम विंडो में टाइमस्टैम्प गिनें।

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

ट्रेड-ऑफ: बिल्कुल सटीक लेकिन प्रति उपयोगकर्ता O(n) प्रविष्टियों को संग्रहीत करता है जहां n अनुरोध गणना है। 10,000 उपयोगकर्ताओं के बीच 1,000 आरपीएस पर, आपकी रेडिस मेमोरी तेजी से बढ़ती है। पासवर्ड रीसेट जैसे कम-वॉल्यूम, उच्च-सुरक्षा एंडपॉइंट के लिए उपयोग करें।

स्लाइडिंग विंडो काउंटर

दो स्थिर विंडो का उपयोग करके अनुमानित स्लाइडिंग विंडो - कोई स्मृति विस्फोट नहीं।

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

यह वह एल्गोरिथम है जिसका उपयोग Cloudflare करता है। यह न्यूनतम ओवरहेड के साथ सीमा स्पाइक को सुचारू करता है - प्रति उपयोगकर्ता प्रति विंडो दो रेडिस कुंजियाँ।

टोकन बकेट

दीर्घकालिक दर को बनाए रखते हुए विस्फोट की अनुमति देने का स्वर्ण मानक। प्रत्येक उपयोगकर्ता के पास एक बाल्टी होती है जो स्थिर दर से भरती है। अनुरोध टोकन का उपभोग करते हैं।

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

टोकन बकेट उन एपीआई के लिए आदर्श है, जिन्हें निरंतर दुरुपयोग को रोकते हुए शॉर्ट बर्स्ट (एक साथ 10 फ़ाइलें अपलोड करना) की अनुमति देने की आवश्यकता होती है।


नेस्टजेएस थ्रॉटलर कॉन्फ़िगरेशन

@nestjs/throttler v5 रेडिस स्टोरेज एडॉप्टर के साथ आता है। यहां एक उत्पादन-तैयार सेटअप है:

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

प्रति नियंत्रक या रूट सीमा ओवरराइड करें:

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

कस्टम कुंजी जेनरेटर

डिफ़ॉल्ट रूप से, NestJS थ्रॉटलर क्लाइंट IP का उपयोग करता है। Nginx/Cloudflare के पीछे उत्पादन में, आपको X-Real-IP या 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);
  }
}

प्रतिक्रिया शीर्षलेख

RFC 6585 और ड्राफ्ट रेटलिमिट हेडर ग्राहकों को सटीक रूप से बताते हैं कि कब पुनः प्रयास करना है:

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

समापन बिंदु-विशिष्ट रणनीतियाँ

अलग-अलग समापन बिंदु अलग-अलग सीमाओं की गारंटी देते हैं। यहां सामान्य पैटर्न के लिए एक संदर्भ तालिका दी गई है:

समापन बिंदु प्रकारएल्गोरिथमसीमाखिड़की
लॉगिन/पासवर्ड रीसेटस्लाइडिंग विंडो लॉग515 मिनट
ओटीपी/2एफए सत्यापित करेंस्थिर खिड़की310 मिनट
सार्वजनिक पढ़ें एपीआईटोकन बाल्टी1000 बर्स्ट, 100/एस भरें
उत्परिवर्तन एपीआई (प्रमाणीकृत)स्लाइडिंग विंडो काउंटर3001 मिनट
वेबहुक अंतर्ग्रहणस्थिर खिड़की10,0001 मिनट
फ़ाइल अपलोडटोकन बाल्टी10 फट, 1/s भरें
एआई/एलएलएम समापन बिंदुस्थिर खिड़की201 मिनट
खोजें (गुमनाम)स्थिर खिड़की301 मिनट

वितरित सुरक्षा के लिए परमाणु लुआ स्क्रिप्ट

जब आपके पास एकाधिक एपीआई प्रतिकृतियां होती हैं, तो वृद्धि-जांच अनुक्रमों में दौड़ की स्थिति सीमा से ऊपर फटने की अनुमति दे सकती है। चेक-एंड-इंक्रीमेंट को परमाणु बनाने के लिए redis.defineCommand के माध्यम से लोड की गई लुआ स्क्रिप्ट का उपयोग करें:

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

सुंदर पतन और बाईपास रणनीतियाँ

दर सीमित करने से आंतरिक स्वास्थ्य जांच, निगरानी एजेंटों या विश्वसनीय भागीदारों को अवरुद्ध नहीं किया जाना चाहिए।

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

प्रगतिशील दर सीमित करने के लिए (हार्ड ब्लॉक से पहले चेतावनी दें), सीमा के 80% को पार करने के बाद ही Retry-After हेडर के साथ 429 लौटाएँ:

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

परीक्षण दर सीमित करना

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

निगरानी और चेतावनी

दर सीमा घटनाएँ मूल्यवान संकेत हैं। उन्हें अपने अवलोकन मंच पर लॉग इन करें:

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

डैशबोर्ड ट्रैकिंग बनाएं:

  • प्रति समापन बिंदु दर सीमा हिट (पी95, पी99)
  • शीर्ष आईपी/उपयोगकर्ता सीमा पार कर रहे हैं
  • अवरुद्ध बनाम सेवा किए गए अनुरोधों का प्रतिशत
  • गलत कॉन्फ़िगर किए गए ग्राहकों का पता लगाने के लिए पुनः प्रयास के बाद की अवधि

अक्सर पूछे जाने वाले प्रश्न

क्या मुझे आईपी या उपयोगकर्ता आईडी के आधार पर सीमा निर्धारित करनी चाहिए?

दोनों का प्रयोग करें. अप्रमाणित समापन बिंदुओं के लिए, आईपी ही एकमात्र उपलब्ध पहचानकर्ता है। प्रमाणित समापन बिंदुओं के लिए, हमेशा उपयोगकर्ता आईडी को प्राथमिकता दें - यह अधिक सटीक है और एक साझा आईपी (कॉर्पोरेट NAT की तरह) को सभी कर्मचारियों को ब्लॉक करने से रोकता है। दो-स्तरीय जाँच लागू करें: Nginx स्तर पर IP सीमा और एप्लिकेशन स्तर पर उपयोगकर्ता-आईडी सीमा।

दर सीमित करने के लिए सही HTTP स्थिति कोड क्या है?

हमेशा 429 Too Many Requests प्रति RFC 6585। कभी भी 503 Service Unavailable (बुनियादी ढांचे की विफलता का मतलब) या 403 Forbidden (प्राधिकरण की विफलता का मतलब) का उपयोग न करें। Retry-After को सेकंडों में हेडर के रूप में शामिल करें ताकि ग्राहकों को पता चले कि कब पुनः प्रयास करना है।

मैं क्लाउडफ्लेयर या लोड बैलेंसर के पीछे दर सीमित करने को कैसे संभालूं?

अपने प्रॉक्सी को X-Real-IP या CF-Connecting-IP सेट करने के लिए कॉन्फ़िगर करें और केवल अपने प्रॉक्सी की IP रेंज पर भरोसा करें। Nginx में: set_real_ip_from 103.21.244.0/22; real_ip_header CF-Connecting-IP;। NestJS में, app.set('trust proxy', 1) सेट करें और req.ip पढ़ें जिसे NestJS विश्वसनीय प्रॉक्सी हेडर से हल करता है।

दर सीमित करने के लिए कौन सी रेडिस डेटा संरचना सर्वोत्तम है?

फिक्स्ड/स्लाइडिंग विंडो काउंटरों के लिए, प्रति अनुरोध एक स्ट्रिंग कुंजी - O(1) पर INCR + EXPIRE का उपयोग करें। स्लाइडिंग विंडो लॉग के लिए, एक क्रमबद्ध सेट (ZADD, ZREMRANGEBYSCORE, ZCARD) - O(लॉग एन) का उपयोग करें। टोकन बकेट के लिए, हैश (HSET, HGET) - O(1) का उपयोग करें। लुआ स्क्रिप्ट तीनों पैटर्न में सभी ऑपरेशनों को परमाणु बनाती है।

मुझे विश्वसनीय प्रदाताओं से वेबहुक के लिए दर सीमा को कैसे संभालना चाहिए?

स्ट्राइप, गिटहब और इसी तरह के प्रदाता ज्ञात आईपी रेंज से वेबहुक भेजते हैं। अपने वेबहुक अंतर्ग्रहण समापन बिंदु पर उन आईपी के लिए उनकी सीआईडीआर श्रेणियों की एक अनुमत सूची बनाए रखें और दर सीमित करने को बायपास करें। पहले वेबहुक हस्ताक्षर सत्यापित करें - हस्ताक्षर सत्यापन आपकी वास्तविक सुरक्षा परत है, न कि दर सीमित करना।

क्या मैं इसके बजाय Nginx स्तर पर दर सीमित लागू कर सकता हूं?

हाँ, और आपको बुनियादी DDoS सुरक्षा के लिए ऐसा करना चाहिए। मोटे आईपी-आधारित सीमाओं (1000 req/min) के लिए Nginx में limit_req_zone का उपयोग करें। प्रति-उपयोगकर्ता, प्रति-एंडपॉइंट नियंत्रण के लिए शीर्ष पर लेयर एप्लिकेशन-स्तरीय दर सीमित करना। दोनों परतें एक-दूसरे की पूरक हैं: Nginx आपके एप्लिकेशन को प्रभावित किए बिना सस्ते में वॉल्यूम हमलों को संभालता है, और NestJS सूक्ष्म व्यावसायिक तर्क सीमाओं को संभालता है।


अगले चरण

मजबूत दर सीमा के बिना उत्पादन एपीआई बनाना आपके सामने वाले दरवाजे को खुला छोड़ने जैसा है। इस गाइड में पैटर्न - तेज़ ट्रैफ़िक के लिए टोकन बकेट, सुचारू प्रवर्तन के लिए स्लाइडिंग विंडो काउंटर, रेडिस-समर्थित वितरित भंडारण, और उचित 429 प्रतिक्रियाएँ - एक सुरक्षित, स्केलेबल एपीआई की रीढ़ बनाते हैं।

ECOSIRE ने रेट लिमिटिंग, रेडिस कैशिंग और पहले दिन से ही पूर्ण अवलोकन क्षमता के साथ एंटरप्राइज़-ग्रेड NestJS बैकएंड का निर्माण किया है। यदि आप एक नया एपीआई लॉन्च कर रहे हैं या किसी मौजूदा को सख्त कर रहे हैं, तो यह देखने के लिए कि हम आपकी डिलीवरी को कैसे तेज कर सकते हैं, हमारी बैकएंड इंजीनियरिंग सेवाओं का पता लगाएं

शेयर करें:
E

लेखक

ECOSIRE Research and Development Team

ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।

WhatsApp पर चैट करें