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 בצד הלקוחות.
