Rate Limiting

Rate limiting היא טכניקה בסיסית של בקרת תעבורה המגבילה את מספר הבקשות שלקוח יכול לשלוח ל-API או לשירות web בתוך חלון זמן מסוים, ומגינה על התשתית מפני עומס יתר, מתקפות מניעת שירות (DoS/DDoS), שימוש לרעה אוטומטי, scraping זדוני ושימוש לא תקין במשאבי חישוב מוגבלים. ב תרחיש שבו APIs מודרניים משרתים מיליוני בקשות בשנייה המגיעות מאפליקציות מובייל, אינטגרציות של שותפים, בוטים לגיטימיים ובאופן פוטנציאלי תוקפים זדוניים, היעדר rate limiting הולם עלול לגרום לעלויות תפעוליות מתפוצצות, לפגיעה חמורה בביצועים עבור משתמשים לגיטימיים, לפגיעוּת למתקפות credential stuffing וכוח גס, ואף לאי-זמינות מוחלטת של השירות. יישום יעיל של rate limiting דורש הבנה מעמיקה של אלגוריתמים שונים (token bucket, leaky bucket, fixed window, sliding window), שיקולים בנוגע לארכיטקטורה מבוזרת שבה שרתים מרובים צריכים לשתף מוני קצב, אסטרטגיות לזיהוי לקוחות (IP, API key, JWT, session), מדיניות מובחנת לפי רמת משתמש (free, premium, enterprise), ומנגנונים לתקשורת ברורה באמצעות HTTP headers סטנדרטיים המודיעים ללקוחות על המגבלות, הצריכה הנוכחית וזמן האיפוס. מאמר זה בוחן אלגוריתמים, דפוסי יישום, כלים ו-best practices לבניית מערכות rate limiting חזקות.

אלגוריתמים של Rate Limiting

1. Token Bucket (הנפוץ ביותר)

      # מושג: Bucket בעל קיבולת מקסימלית של tokens
      # Tokens מתווספים בקצב קבוע
      # כל request צורך 1 token
      # אם ה-bucket ריק, ה-request נדחה
      קיבולת: 100 tokens
      קצב מילוי מחדש: 10 tokens/שנייה
      יתרונות:
      - מאפשר bursts מבוקרים (bucket מלא)
      - פשוט ליישום
      - גמיש לדפוסי תעבורה שונים
      חסרונות:
      - עלול לאפשר bursts שמעמיסים על המערכת
      - דורש tracking של המילוי מחדש האחרון
      יישום:
      class TokenBucket {
      constructor(capacity, refillRate) {
      this.capacity = capacity;
      this.tokens = capacity;
      this.refillRate = refillRate;
      this.lastRefill = Date.now();
      }
      consume(count = 1) {
      this.refill();
      if (this.tokens >= count) {
      this.tokens -= count;
      return true;
      }
      return false;
      }
      refill() {
      const now = Date.now();
      const elapsed = (now - this.lastRefill) / 1000;
      const tokensToAdd = elapsed * this.refillRate;
      this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
      this.lastRefill = now;
      }
      }
      

2. Leaky Bucket

      # מושג: Queue עם דליפה קבועה
      # Requests נכנסים ל-bucket (queue)
      # מעובדים בקצב קבוע (leak)
      # אם ה-queue מלא, ה-requests נדחים
      יתרונות:
      - מחליק bursts (traffic shaping)
      - Output rate קבוע וצפוי
      - מגן על ה-backend מפני spikes
      חסרונות:
      - עלול להוסיף latency (queueing)
      - מורכבות יישום
      שימוש:
      - Traffic shaping
      - Network gateways
      - כאשר output rate קבוע הוא קריטי
      

3. Fixed Window Counter

      # מושג: מונה לכל חלון זמן קבוע
      # דוגמה: 100 requests לדקה
      # איפוס בתחילת כל דקה (XX:00, XX:01, XX:02...)
      יתרונות:
      - פשוט במיוחד
      - Memory efficient
      - קל להבנה
      חסרונות:
      - Edge case: 200 requests בשנייה אחת
      (100 בסוף דקה 1, 100 בתחילת דקה 2)
      - מאפשר bursts בגבול החלונות
      יישום Redis:
      INCR user:123:2024-01-15:14:30
      EXPIRE user:123:2024-01-15:14:30 60
      GET user:123:2024-01-15:14:30  # אם > 100, reject
      

4. Sliding Window Log

      # מושג: Log של timestamps של requests
      # מסיר requests מחוץ לחלון
      # סופר requests בתוך החלון הנגלל
      יתרונות:
      - דיוק מושלם
      - ללא edge cases של fixed window
      - מחלק את הקצב באופן אחיד
      חסרונות:
      - Memory intensive (מאחסן את כל ה-timestamps)
      - הביצועים מתדרדרים עם high traffic
      יישום Redis (Sorted Set):
      ZADD user:123 <timestamp> <request-id>
      ZREMRANGEBYSCORE user:123 0 <timestamp-60s>  # מסיר ישנים
      ZCARD user:123  # Count requests
      אם > 100, reject
      

5. Sliding Window Counter (היברידי)

      # מושג: משלב fixed window עם sliding
      # משתמש במוני חלונות קודמים עם משקל
      # מעריך את הקצב בחלון נגלל
      דוגמה: מגבלה 100/דקה
      חלון נוכחי (14:30): 70 requests
      חלון קודם (14:29): 90 requests
      זמן שחלף בחלון הנוכחי: 40s (66.7%)
      הערכה: 90 * (1 - 0.667) + 70 = 30 + 70 = 100
      יתרונות:
      - דיוק קרוב ל-sliding log
      - Memory efficient (רק 2 מונים)
      - מחליק bursts
      חסרונות:
      - הערכה (לא מדויק)
      - מורכב יותר מ-fixed window
      

Distributed Rate Limiting

      # בעיה: שרתים מרובים צריכים לשתף state
      # פתרונות:
      1. Redis מרכזי (הנפוץ ביותר)
      const redis = require('redis');
      const client = redis.createClient();
      async function checkRateLimit(userId) {
      const key = \`rate:\$:\${getCurrentWindow()}\`;
      const count = await client.incr(key);
      if (count === 1) {
      await client.expire(key, 60); // 60 seconds
      }
      return count <= 100; // Limit: 100/min
      }
      2. Redis Lua Script (Atomic)
      const luaScript = \`
      local key = KEYS[1]
      local limit = tonumber(ARGV[1])
      local current = redis.call('incr', key)
      if current == 1 then
      redis.call('expire', key, ARGV[2])
      end
      if current > limit then
      return 0
      end
      return 1
      \`;
      3. Sticky Sessions + Local Counters
      - לנתב את המשתמש תמיד לאותו server
      - Counter מקומי בשרת
      - בעיה: לא עובד היטב עם auto-scaling
      4. Gossip Protocol
      - שרתים משתפים state באמצעות gossip
      - Eventual consistency
      - מורכב יותר, בשימוש ב-high scale
      

HTTP Headers סטנדרטיים

      # Standards (RFCs)
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 45
      X-RateLimit-Reset: 1640000000  # Unix timestamp
      # כאשר המגבלה חורגת
      HTTP/1.1 429 Too Many Requests
      Retry-After: 60  # שניות עד ל-retry
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 0
      X-RateLimit-Reset: 1640000060
      {
      "error": "Rate limit exceeded",
      "retryAfter": 60,
      "limit": 100
      }
      # סגנון GitHub (אינפורמטיבי יותר)
      X-RateLimit-Limit: 5000
      X-RateLimit-Remaining: 4999
      X-RateLimit-Reset: 1372700873
      X-RateLimit-Used: 1
      X-RateLimit-Resource: core
      

יישום עם Express.js

      const rateLimit = require('express-rate-limit');
      const RedisStore = require('rate-limit-redis');
      const redis = require('redis');
      const client = redis.createClient();
      // Basic rate limiter
      const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      standardHeaders: true, // Return rate limit info in headers
      legacyHeaders: false,
      message: 'Too many requests, please try again later.'
      });
      app.use('/api/', limiter);
      // Redis-based distributed rate limiting
      const distributedLimiter = rateLimit({
      store: new RedisStore({
      client: client,
      prefix: 'rate-limit:',
      }),
      windowMs: 60 * 1000,
      max: 10,
      standardHeaders: true,
      });
      // Different limits per route
      const authLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 5, // Stricter for auth endpoints
      skipSuccessfulRequests: true, // Don't count successful logins
      });
      app.post('/api/login', authLimiter, loginHandler);
      // Custom key function (rate limit by user ID instead of IP)
      const userLimiter = rateLimit({
      windowMs: 60 * 1000,
      max: 100,
      keyGenerator: (req) => req.user.id, // Requires auth middleware
      });
      

Tiered Rate Limiting

      // Different limits based on user tier
      function getRateLimit(user) {
      const tiers = {
      free: { windowMs: 3600000, max: 100 },      // 100/hour
      basic: { windowMs: 3600000, max: 1000 },    // 1000/hour
      premium: { windowMs: 3600000, max: 10000 }, // 10k/hour
      enterprise: { windowMs: 3600000, max: 100000 } // 100k/hour
      };
      return tiers[user.tier] || tiers.free;
      }
      app.use(async (req, res, next) => {
      const user = await getUserFromToken(req);
      const limits = getRateLimit(user);
      const limiter = rateLimit({
      ...limits,
      keyGenerator: () => user.id,
      });
      limiter(req, res, next);
      });
      

כלים ושירותים

  • Redis: מונים מבוזרים, expire אוטומטי
  • Kong: API Gateway עם plugin של rate limiting
  • Nginx rate limiting: limit_req_zone, limit_conn_zone
  • Cloudflare: Rate limiting ברמת ה-CDN
  • AWS API Gateway: Throttling מובנה
  • express-rate-limit: Middleware של Express
  • Tyk: API gateway בקוד פתוח

Best Practices

  • בחרו אלגוריתם מתאים: Token bucket ל-APIs כלליים, sliding window לדיוק
  • מגבלות מובחנות: Endpoints של auth מגבילים יותר, read-only מתירניים יותר
  • תקשורת ברורה: Headers אינפורמטיביים, הודעות שגיאה שימושיות
  • Whitelist: כתובות IP של שותפים מהימנים, health checks
  • Monitoring: התרעה כאשר משתמשים מגיעים למגבלות בתדירות גבוהה
  • Graceful degradation: החזירו cached data אם אפשר
  • Distributed state: השתמשו ב-Redis עבור deployments מרובי-שרתים
  • Cost-based limiting: פעולות יקרות צורכות יותר tokens

המלצות

עבור APIs מודרניים, יישמו token bucket עם Redis עבור distributed rate limiting. השתמשו ב-sliding window counter כאשר אתם זקוקים לדיוק ללא overhead של זיכרון. הגדירו מגבלות מובחנות: 5 req/min ל-login, 100 req/min לקריאה, 10 req/min לפעולות כתיבה. החזירו תמיד headers אינפורמטיביים ויישמו retry logic עם exponential backoff בצד הלקוחות.