Concept #026
SécuritéLe Rate Limiting
- sécurité
- api
- performance
Le principe en une phrase
Le Rate Limiting consiste à limiter le nombre de requêtes qu'un client peut envoyer à un service dans un intervalle de temps donné. Au-delà de la limite, les requêtes sont rejetées (généralement avec un code HTTP 429).
C'est l'assurance-vie de vos ressources. Sans rate limiting, un seul utilisateur (ou bot) peut monopoliser votre serveur, ralentir le service pour tout le monde, ou pire -- exploiter vos endpoints sensibles par force brute.
Pourquoi le Rate Limiting est indispensable
Protection contre les attaques par force brute
Un formulaire de connexion sans rate limiting est une invitation ouverte. Un attaquant peut tester des milliers de combinaisons login/mot de passe par seconde.
POST /api/login { "email": "admin@site.com", "password": "aaa" } → 401
POST /api/login { "email": "admin@site.com", "password": "aab" } → 401
POST /api/login { "email": "admin@site.com", "password": "aac" } → 401
... 10 000 tentatives plus tard ...
POST /api/login { "email": "admin@site.com", "password": "P@ss1" } → 200 !!!
Avec un rate limit de 5 tentatives par minute sur /api/login, l'attaque devient impraticable. Il faudrait des mois au lieu de quelques minutes.
Protection contre le scraping
Sans limite, un concurrent peut aspirer l'intégralité de votre catalogue produit, vos prix, vos contenus -- en quelques heures avec un script automatisé.
Protection de la disponibilité
Un pic de trafic (légitime ou non) peut saturer vos serveurs. Le rate limiting protège les ressources pour que le service reste disponible pour l'ensemble des utilisateurs.
Les algorithmes de Rate Limiting
1. Fenêtre fixe (Fixed Window)
L'approche la plus simple : on découpe le temps en fenêtres fixes (par exemple, chaque minute) et on compte les requêtes dans chaque fenêtre.
class FixedWindowLimiter {
private counts = new Map<string, { count: number; windowStart: number }>();
constructor(
private readonly maxRequests: number,
private readonly windowMs: number,
) {}
isAllowed(clientId: string): boolean {
const now = Date.now();
const entry = this.counts.get(clientId);
// Nouvelle fenêtre ou premier appel
if (!entry || now - entry.windowStart >= this.windowMs) {
this.counts.set(clientId, { count: 1, windowStart: now });
return true;
}
// Dans la fenêtre courante
if (entry.count < this.maxRequests) {
entry.count++;
return true;
}
return false; // Limite atteinte
}
}
const limiter = new FixedWindowLimiter(100, 60_000); // 100 req/minInconvénient : le problème de la "frontière de fenêtre". Un client peut envoyer 100 requêtes à 12:00:59, puis 100 autres à 12:01:00 -- soit 200 requêtes en 2 secondes. La fenêtre a changé, mais le pic est bien réel.
2. Fenêtre glissante (Sliding Window Log)
On enregistre le timestamp de chaque requête et on compte celles dans les N dernières secondes.
class SlidingWindowLimiter {
private logs = new Map<string, number[]>();
constructor(
private readonly maxRequests: number,
private readonly windowMs: number,
) {}
isAllowed(clientId: string): boolean {
const now = Date.now();
const timestamps = this.logs.get(clientId) ?? [];
// Supprimer les entrées hors de la fenêtre
const validTimestamps = timestamps.filter(
(t) => now - t < this.windowMs,
);
if (validTimestamps.length < this.maxRequests) {
validTimestamps.push(now);
this.logs.set(clientId, validTimestamps);
return true;
}
this.logs.set(clientId, validTimestamps);
return false;
}
}Plus précis que la fenêtre fixe, mais consomme plus de mémoire car il faut stocker chaque timestamp.
3. Token Bucket
Le client dispose d'un "seau" de jetons. Chaque requête consomme un jeton. Les jetons se régénèrent à un rythme constant. Si le seau est vide, la requête est refusée.
class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private readonly capacity: number,
private readonly refillRate: number, // tokens par seconde
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
consume(): boolean {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return true;
}
return false;
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsed * this.refillRate,
);
this.lastRefill = now;
}
}
// 10 tokens max, recharge de 2 tokens/seconde
const bucket = new TokenBucket(10, 2);L'avantage du Token Bucket : il autorise des rafales (bursts) tant que le seau n'est pas vide, tout en limitant le débit moyen. C'est l'algorithme utilisé par la plupart des grandes API (AWS, Stripe, GitHub).
Implémentation avec Express.js
En pratique, on utilise un middleware dédié :
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import { createClient } from "redis";
const redisClient = createClient({ url: "redis://localhost:6379" });
// Rate limit global : 100 requêtes par minute
const globalLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true, // en-têtes RateLimit-*
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
message: { error: "Trop de requêtes. Réessayez dans une minute." },
});
// Rate limit strict sur le login : 5 tentatives par minute
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // ne compte que les échecs
message: { error: "Trop de tentatives. Compte temporairement verrouillé." },
});
app.use(globalLimiter);
app.post("/api/login", loginLimiter, loginHandler);L'utilisation de Redis comme store est essentielle en production : si vous avez plusieurs instances de votre serveur (derrière un load balancer), le compteur doit être partagé.
Les en-têtes HTTP standard
Les API bien conçues communiquent l'état du rate limiting via des en-têtes HTTP :
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1706889600
Et quand la limite est atteinte :
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 1706889600
{"error": "Rate limit exceeded. Try again in 30 seconds."}
Le header Retry-After indique au client quand il peut réessayer. Les clients bien implémentés respectent cette valeur et attendent automatiquement.
Stratégies de limitation
Le rate limiting ne se limite pas à un compteur global. Selon le contexte, on adapte la stratégie :
- Par adresse IP : la plus courante, mais attention aux utilisateurs derrière un NAT ou un VPN d'entreprise qui partagent la même IP.
- Par utilisateur authentifié : plus précis, indépendant de l'IP. Un utilisateur premium peut avoir des limites plus élevées.
- Par endpoint :
/api/logina des limites strictes,/api/productsest plus permissif. - Par plan tarifaire : 1000 requêtes/jour pour le plan gratuit, 100 000 pour le plan entreprise. C'est le modèle de la plupart des API SaaS.
Rate Limiting vs Throttling
Les deux concepts sont proches mais distincts :
| Rate Limiting | Throttling | |
|---|---|---|
| Action | Rejette les requêtes excédentaires | Ralentit les requêtes excédentaires |
| Réponse | 429 Too Many Requests | Mise en file d'attente ou délai |
| Expérience client | Erreur explicite | Latence accrue |
En pratique, les deux sont souvent combinés : on throttle d'abord (file d'attente), puis on rate-limite si la file déborde.
Résumé
Le Rate Limiting n'est pas un luxe, c'est une nécessité. Il protège contre les attaques par force brute, empêche le scraping intensif, et garantit la disponibilité du service pour l'ensemble des utilisateurs. Combiné aux bons en-têtes HTTP et à un stockage partagé (Redis), il devient une brique fondamentale de toute API sérieuse.