Rate Limiting
Le rate limiting est une technique fondamentale de contrôle du trafic qui limite le nombre de requêtes qu'un client peut effectuer vers une API ou un service web au cours d'une fenêtre de temps spécifique, protégeant l'infrastructure contre la surcharge, les attaques par déni de service (DoS/DDoS), l'abus automatisé, le scraping malveillant et l'utilisation inappropriée de ressources de calcul limitées. Dans un scénario où les API modernes traitent des millions de requêtes par seconde provenant d'applications mobiles, d'intégrations de partenaires, de bots légitimes et potentiellement d'attaquants malveillants, l'absence d'un rate limiting adéquat peut entraîner des coûts opérationnels explosifs, une dégradation sévère des performances pour les utilisateurs légitimes, des vulnérabilités aux attaques par credential stuffing et par force brute, et même l'indisponibilité totale du service. La mise en œuvre efficace du rate limiting nécessite une compréhension approfondie des différents algorithmes (token bucket, leaky bucket, fixed window, sliding window), des considérations sur l'architecture distribuée où plusieurs serveurs doivent partager des compteurs de débit, des stratégies d'identification des clients (IP, API key, JWT, session), des politiques différenciées par niveau d'utilisateur (free, premium, enterprise), et des mécanismes de communication claire via des HTTP headers standardisés qui informent les clients sur les limites, la consommation actuelle et le temps de réinitialisation. Cet article explore les algorithmes, les patterns d'implémentation, les outils et les best practices pour construire des systèmes robustes de rate limiting.
Algorithmes de Rate Limiting
1. Token Bucket (Le Plus Courant)
# Concept : Bucket avec capacité maximale de tokens
# Les tokens sont ajoutés à débit constant
# Chaque request consomme 1 token
# Si le bucket est vide, le request est rejeté
Capacité : 100 tokens
Débit de recharge : 10 tokens/seconde
Avantages :
- Permet des bursts contrôlés (bucket plein)
- Simple à implémenter
- Flexible pour différents patterns de trafic
Inconvénients :
- Peut permettre des bursts qui surchargent le système
- Nécessite le tracking de la dernière recharge
Implémentation :
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
# Concept : Queue avec écoulement constant
# Les requests entrent dans le bucket (queue)
# Traités à débit constant (leak)
# Si la queue est pleine, les requests sont rejetés
Avantages :
- Lisse les bursts (traffic shaping)
- Output rate constant et prévisible
- Protège le backend des spikes
Inconvénients :
- Peut ajouter de la latence (queueing)
- Complexité d'implémentation
Usage :
- Traffic shaping
- Network gateways
- Quand un output rate constant est critique
3. Fixed Window Counter
# Concept : Compteur par fenêtre de temps fixe
# Exemple : 100 requests par minute
# Réinitialisation au début de chaque minute (XX:00, XX:01, XX:02...)
Avantages :
- Extrêmement simple
- Memory efficient
- Facile à comprendre
Inconvénients :
- Edge case : 200 requests en 1 seconde
(100 à la fin de la minute 1, 100 au début de la minute 2)
- Permet des bursts à la limite des fenêtres
Implémentation 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 # Si > 100, reject
4. Sliding Window Log
# Concept : Log des timestamps des requests
# Supprime les requests en dehors de la fenêtre
# Compte les requests à l'intérieur de la fenêtre glissante
Avantages :
- Précision parfaite
- Pas d'edge cases de fixed window
- Distribue le débit uniformément
Inconvénients :
- Memory intensive (stocke tous les timestamps)
- Les performances se dégradent avec un high traffic
Implémentation Redis (Sorted Set) :
ZADD user:123 <timestamp> <request-id>
ZREMRANGEBYSCORE user:123 0 <timestamp-60s> # Supprime les anciens
ZCARD user:123 # Count requests
Si > 100, reject
5. Sliding Window Counter (Hybride)
# Concept : Combine fixed window avec sliding
# Utilise les compteurs des fenêtres précédentes avec pondération
# Estime le débit dans une sliding window
Exemple : Limite 100/minute
Fenêtre actuelle (14:30) : 70 requests
Fenêtre précédente (14:29) : 90 requests
Écoulé dans la fenêtre actuelle : 40s (66.7%)
Estimation : 90 * (1 - 0.667) + 70 = 30 + 70 = 100
Avantages :
- Précision proche de sliding log
- Memory efficient (seulement 2 compteurs)
- Lisse les bursts
Inconvénients :
- Estimation (pas exact)
- Plus complexe que fixed window
Distributed Rate Limiting
# Problème : Plusieurs serveurs doivent partager le state
# Solutions :
1. Redis Centralisé (Le Plus Courant)
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
- Router l'utilisateur toujours vers le même server
- Counter local sur le serveur
- Problème : Ne fonctionne pas bien avec l'auto-scaling
4. Gossip Protocol
- Les serveurs partagent le state via gossip
- Eventual consistency
- Plus complexe, utilisé en high scale
HTTP Headers Standards
# Standards (RFCs)
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000 # Unix timestamp
# Quand la limite est dépassée
HTTP/1.1 429 Too Many Requests
Retry-After: 60 # Secondes avant le retry
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000060
{
"error": "Rate limit exceeded",
"retryAfter": 60,
"limit": 100
}
# Style GitHub (plus informatif)
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873
X-RateLimit-Used: 1
X-RateLimit-Resource: core
Implémentation avec 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);
});
Outils et Services
- Redis : Compteurs distribués, expire automatique
- Kong : API Gateway avec plugin de rate limiting
- Nginx rate limiting : limit_req_zone, limit_conn_zone
- Cloudflare : Rate limiting au niveau du CDN
- AWS API Gateway : Throttling intégré
- express-rate-limit : Middleware Express
- Tyk : API gateway open-source
Best Practices
- Choisissez l'algorithme adéquat : Token bucket pour les API générales, sliding window pour la précision
- Limites différenciées : Endpoints d'auth plus restrictifs, read-only plus permissifs
- Communication claire : Headers informatifs, messages d'erreur utiles
- Whitelist : IPs de partenaires de confiance, health checks
- Monitoring : Alerter lorsque les utilisateurs atteignent fréquemment les limites
- Graceful degradation : Retourner des cached data si possible
- Distributed state : Utiliser Redis pour les déploiements multi-serveurs
- Cost-based limiting : Les opérations coûteuses consomment plus de tokens
Recommandations
Pour les API modernes, implémentez un token bucket avec Redis pour le distributed rate limiting. Utilisez un sliding window counter lorsque vous avez besoin de précision sans overhead de mémoire. Configurez des limites différenciées : 5 req/min pour le login, 100 req/min pour la lecture, 10 req/min pour les opérations d'écriture. Retournez toujours des headers informatifs et implémentez une retry logic avec exponential backoff côté clients.
