13 - Rate limiting : protéger ton API sans frustrer tes clients
Ce que tu vas apprendre
- Les algorithmes de rate limiting (token bucket, sliding window)
- Les headers de réponse X-RateLimit-* et Retry-After
- La différence entre limiter par user, par IP et par API key
- Comment implementer un rate limiter avec Redis
Prerequisites
Avoir lu l'article sur CORS. Connaitre les bases de Redis (SET, GET, EXPIRE) est un plus mais pas obligatoire.
Un lundi matin, un de nos clients a lance un script d'import qui envoyait 3000 requêtes par seconde a notre API. Pas de malice, juste un boucle sans await. En dix minutes, le serveur etait a genoux et tous les autres clients subissaient des timeouts. On n'avait pas de rate limiting. On en a ajoute un le jour meme.
Pourquoi limiter le debit
Sans rate limiting, un seul client peut monopoliser toutes tes ressources. Que ce soit un script mal écrit, un bot, ou une attaque DDoS, le résultat est le meme : ton API devient inutilisable pour tout le monde. Le rate limiting pose une regle simple : X requêtes maximum par fenêtre de temps.
Les algorithmes
Token bucket
Imagine un seau qui contient des jetons. Chaque requête consomme un jeton. Le seau se remplit a un rythme constant. Si le seau est vide, la requête est refusee.
Capacite : 100 jetons
Remplissage : 10 jetons/seconde
Requete entrante : -1 jeton
Seau vide : 429 Too Many Requests
L'avantage du token bucket : il autorise les petits bursts. Si ton client n'a rien envoye pendant 10 secondes, il a 100 jetons disponibles et peut envoyer un burst de 100 requêtes. C'est un comportement naturel et souhaitable.
Sliding window
Tu comptes les requêtes dans une fenêtre glissante. Par exemple, "100 requêtes max dans les 60 dernières secondes". A chaque requête, tu regardes combien ont ete envoyees dans la fenêtre.
Fenetre : 60 secondes
Limite : 100 requetes
Requete a t=61s : on retire les requetes avant t=1s du compteur
Le sliding window est plus strict que le token bucket : pas de burst possible. Mais il est plus simple a comprendre et a expliquer dans ta documentation.
Fixed window
La version simplifiee : tu decoupes le temps en fenêtres fixes (par exemple, chaque minute). Tu comptes les requêtes dans la fenêtre en cours. Le problème : un client peut envoyer 100 requêtes a 12:00:59 et 100 autres a 12:01:01, soit 200 en 2 secondes. Le sliding window évité ce problème.
Les headers de réponse
La convention (pas un standard officiel, mais adoptee par tout le monde) utilise trois headers :
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1711699200
- X-RateLimit-Limit : le nombre max de requêtes autorisees
- X-RateLimit-Remaining : combien il en reste
- X-RateLimit-Reset : le timestamp Unix ou la fenêtre se reinitialise
Quand la limite est atteinte, tu retournes un 429 Too Many Requests avec un header Retry-After qui indique combien de secondes attendre :
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711699200
Content-Type: application/problem+json
{
"type": "https://api.paltemps.fr/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Tu as depasse la limite de 100 requetes par minute. Reessaie dans 30 secondes."
}
Par user, par IP ou par API key ?
Trois stratégies, chacune avec ses compromis :
Par IP : simple a implementer, mais un NAT d'entreprise peut cacher 500 utilisateurs derrière une seule IP. Tu risques de bloquer tout un bureau.
Par user authentifié : plus juste, car chaque utilisateur a son propre compteur. Mais ca ne protégé pas les endpoints publics (login, inscription).
Par API key : le meilleur compromis pour les API B2B. Chaque client a sa clé avec son propre quota. Tu peux meme vendre des tiers différents (free : 100 req/min, pro : 1000 req/min).
En pratique, je combine : rate limit par IP sur les endpoints publics, par user/API key sur les endpoints authentifies.
Implementation avec Redis
Redis est parfait pour le rate limiting : rapide, atomique, et il gere nativement l'expiration des clés.
typescriptimport Redis from "ioredis";
const redis = new Redis();
async function rateLimit(key: string, limit: number, windowSec: number) {
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSec);
}
return {
allowed: current <= limit,
remaining: Math.max(0, limit - current),
resetAt: Math.floor(Date.now() / 1000) + windowSec
};
}
// Middleware Express
app.use(async (req, res, next) => {
const key = `rate:${req.ip}`;
const result = await rateLimit(key, 100, 60);
res.set("X-RateLimit-Limit", "100");
res.set("X-RateLimit-Remaining", String(result.remaining));
res.set("X-RateLimit-Reset", String(result.resetAt));
if (!result.allowed) {
res.set("Retry-After", "60");
return res.status(429).json({
type: "https://api.example.com/errors/rate-limit-exceeded",
title: "Rate Limit Exceeded",
status: 429,
detail: "Limite de 100 requetes par minute depassee."
});
}
next();
});
Le INCR + EXPIRE est une implementation basique (fixed window). Pour un vrai sliding window, tu utilises un sorted set Redis avec ZADD et ZRANGEBYSCORE.
Sliding window avec sorted set
typescriptasync function slidingWindowRateLimit(key: string, limit: number, windowSec: number) {
const now = Date.now();
const windowStart = now - windowSec * 1000;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart);
pipeline.zadd(key, now, `${now}-${Math.random()}`);
pipeline.zcard(key);
pipeline.expire(key, windowSec);
const results = await pipeline.exec();
const count = results[2][1] as number;
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count)
};
}
Des limites différentes par endpoint
Tous les endpoints ne se valent pas. Un GET sur une liste peut etre appele souvent, mais un POST qui envoie un email doit etre beaucoup plus restreint :
typescriptconst limits = {
"GET:/users": { limit: 200, window: 60 },
"POST:/auth/login": { limit: 5, window: 60 },
"POST:/emails/send": { limit: 10, window: 3600 }
};
Le endpoint de login merite une attention particulière : 5 tentatives par minute, c'est suffisant pour un humain et ca bloque le brute force.
Résumé
- Le rate limiting protégé ton API contre les abus et garantit l'equite entre clients
- Token bucket autorise les bursts, sliding window est plus strict mais previsible
- Renvoie toujours les headers X-RateLimit-* et un 429 avec Retry-After
- Combine les stratégies : par IP sur les endpoints publics, par user/API key sur le reste
- Redis est l'outil ideal avec INCR/EXPIRE (fixed window) ou ZADD (sliding window)
Article précédent : CORS Article suivant : Caching