/ src / utils / rate-limiter.js
rate-limiter.js
  1  import Bottleneck from 'bottleneck';
  2  import './load-env.js';
  3  
  4  /**
  5   * Rate limiter for OpenRouter API
  6   *
  7   * Vendor limits:  200 RPM for gpt-4o-mini (paid plan); 20 RPM (free tier)
  8   * Regulatory:     None
  9   *
 10   * Default: OPENROUTER_REQUESTS_PER_MINUTE=194  (3% below 200 RPM paid plan)
 11   *          OPENROUTER_MAX_CONCURRENT=5
 12   * Free tier override: set OPENROUTER_REQUESTS_PER_MINUTE=19
 13   */
 14  export const openRouterLimiter = new Bottleneck({
 15    minTime: Math.round(60000 / parseInt(process.env.OPENROUTER_REQUESTS_PER_MINUTE || '194', 10)),
 16    maxConcurrent: parseInt(process.env.OPENROUTER_MAX_CONCURRENT || '5', 10),
 17  });
 18  
 19  /**
 20   * Rate limiter for Twilio SMS API
 21   *
 22   * Vendor limits:  Long code = 1 SMS/sec per number
 23   *                 Toll-free = 3 SMS/sec
 24   *                 Short code = 100 SMS/sec
 25   * Regulatory:     TCPA/A2P 10DLC — 10 MPS cap for registered campaigns;
 26   *                 business hours (8am–9pm local) enforced in compliance.js
 27   *
 28   * Default: TWILIO_REQUESTS_PER_SECOND=0.7  (30% below 1 SMS/sec long code limit)
 29   *          TWILIO_MAX_CONCURRENT=1
 30   * Short code override: set TWILIO_REQUESTS_PER_SECOND=70
 31   */
 32  export const twilioLimiter = new Bottleneck({
 33    minTime: Math.round(1000 / parseFloat(process.env.TWILIO_REQUESTS_PER_SECOND || '0.7')),
 34    maxConcurrent: parseInt(process.env.TWILIO_MAX_CONCURRENT || '1', 10),
 35  });
 36  
 37  /**
 38   * Rate limiter for ZenRows API
 39   *
 40   * Vendor limits:  Monthly subscription — no daily limit (confirmed with ZenRows support 2026-03-06)
 41   *                 Concurrency: 20 simultaneous requests max
 42   * Regulatory:     None
 43   *
 44   * Default: ZENROWS_CONCURRENCY=19  (5% below 20 concurrent limit)
 45   * No daily reservoir — monthly plan has no daily cap.
 46   *
 47   * Reference: https://docs.zenrows.com/universal-scraper-api/features/concurrency
 48   */
 49  export const zenrowsLimiter = new Bottleneck({
 50    maxConcurrent: parseInt(process.env.ZENROWS_CONCURRENCY || '19', 10),
 51  });
 52  
 53  /**
 54   * Rate limiter for Resend email API
 55   *
 56   * Vendor limits:  10 req/sec (Pro plan); 50,000 emails/month, NO daily cap
 57   * Regulatory:     CAN-SPAM/CASL — no per-second limit; campaign-level frequency
 58   *                 restrictions are enforced via outreach cooldown logic, not here
 59   *
 60   * Default: RESEND_REQUESTS_PER_SECOND=1.5  (25% below 2 req/sec actual limit)
 61   *   NOTE: Resend docs say 10 req/sec but actual plan enforces 2 req/sec
 62   *          RESEND_MAX_CONCURRENT=1
 63   *
 64   * Plan: Pro — 50,000 emails/month, no daily limit (upgraded 2026-03-06)
 65   * Reference: https://resend.com/docs/api-reference/introduction#rate-limit
 66   */
 67  export const resendLimiter = new Bottleneck({
 68    minTime: Math.round(1000 / parseFloat(process.env.RESEND_REQUESTS_PER_SECOND || '1.5')),
 69    maxConcurrent: parseInt(process.env.RESEND_MAX_CONCURRENT || '1', 10),
 70  });
 71  
 72  /**
 73   * Rate limiter for ZeroBounce Email Validation API
 74   *
 75   * Vendor limits:  ~100 req/sec (documented upper bound)
 76   * Note: batch endpoint sends 200 emails per API call, so effective throughput
 77   *       is 200 × 40 = 8,000 emails/sec — far more than needed.
 78   *
 79   * Default: ZEROBOUNCE_REQUESTS_PER_SECOND=40  (60% below vendor limit)
 80   *          ZEROBOUNCE_MAX_CONCURRENT=5
 81   */
 82  export const zeroBounceLimiter = new Bottleneck({
 83    minTime: Math.round(1000 / parseFloat(process.env.ZEROBOUNCE_REQUESTS_PER_SECOND || '40')),
 84    maxConcurrent: parseInt(process.env.ZEROBOUNCE_MAX_CONCURRENT || '5', 10),
 85  });
 86  
 87  /**
 88   * Rate limiter for DataForSEO API
 89   *
 90   * Vendor limits:  2,000 req/min; 100 concurrent connections max
 91   * Regulatory:     None
 92   *
 93   * Default: DATAFORSEO_REQUESTS_PER_MINUTE=1940  (3% below 2,000 RPM vendor limit)
 94   *          DATAFORSEO_MAX_CONCURRENT=97          (3% below 100 concurrent limit)
 95   *
 96   * Reference: https://docs.dataforseo.com/v3/appendix/rate_limits/
 97   */
 98  export const dataForSEOLimiter = new Bottleneck({
 99    minTime: Math.round(60000 / parseInt(process.env.DATAFORSEO_REQUESTS_PER_MINUTE || '1940', 10)),
100    maxConcurrent: parseInt(process.env.DATAFORSEO_MAX_CONCURRENT || '97', 10),
101  });