Rate Limiting Distribué avec Redis : Token Bucket et Fenêtres Glissantes
Protéger les APIs contre les attaques DoS et le brute-force avec des scripts Lua atomiques ultra-rapides.
1. Pourquoi le rate limiting naïf s’effondre en charge
Le premier rate limiter que la plupart des gens écrivent est un compteur en mémoire du processus : une Map indexée par ID utilisateur ou IP, incrémentée à chaque requête, réinitialisée chaque minute. Cela fonctionne en démo et s’effondre dès que vous faites tourner plus d’une instance de votre API - chaque processus a son propre compteur, donc une limite de 100 requêtes/minute devient silencieusement 100 fois N instances, sans erreur ni ligne de log expliquant pourquoi. C’est la raison la plus fréquente pour laquelle un rate limiting en production ne fait strictement rien.
La correction consiste à déplacer l’état du compteur vers un endroit partagé par toutes les instances - en pratique Redis, car l’opération doit être à la fois rapide (sub-milliseconde, sur le chemin critique de chaque requête) et atomique (deux requêtes concurrentes du même client ne doivent pas toutes deux lire le même compte obsolète et être toutes deux admises). Un aller-retour vers Postgres pour cela est possible mais ajoute une latence et une charge qu’on préfère éviter sur chaque requête.
L’atomicité est ce que les implémentations naïves ratent même après le passage à Redis. Un motif lire-puis-écrire (GET count, vérifier le seuil, INCR) exécuté en deux commandes Redis séparées laisse une fenêtre de course : deux requêtes arrivant à la même microseconde peuvent toutes deux lire la même valeur sous la limite et être toutes deux admises, doublant silencieusement votre limite effective sous charge concurrente. La solution consiste à faire le check-and-increment en une seule opération atomique.
2. Token bucket contre fenêtre glissante : choisir le bon algorithme
Le token bucket modélise un seau contenant jusqu’à N tokens, rechargé à taux fixe (par exemple 10 tokens/seconde), où chaque requête consomme un token et est rejetée si le seau est vide. Sa propriété caractéristique est qu’il autorise les rafales : un client resté inactif peut dépenser tout son seau en un instant, puis doit attendre le rechargement. Cela correspond bien aux schémas de trafic réels - un utilisateur cliquant cinq fois d’affilée dans une interface ne devrait pas être throttlé juste parce que c’est arrivé dans la même seconde.
La fenêtre glissante (variante log ou compteur) suit au contraire les requêtes dans une fenêtre temporelle qui se déplace en continu - pas plus de 100 requêtes sur les 60 dernières secondes, mesurées en continu, sans réinitialisation sur la frontière de la minute. Les fenêtres fixes ont un défaut bien connu en cas limite : un client peut envoyer 100 requêtes à 0:59 puis 100 autres à 1:01, respectant techniquement une limite fixe de 100/minute tout en envoyant 200 requêtes en deux secondes. La fenêtre glissante ferme cette brèche en évaluant la limite en continu plutôt qu’à des frontières fixes.
Le choix pratique : token bucket pour lisser un comportement client légitimement en rafale (indicateurs de frappe, actions d’interface groupées) où l’on veut tolérer de courts pics ; fenêtre glissante pour les frontières de sécurité dures (tentatives de connexion, demandes de réinitialisation de mot de passe) où l’on veut spécifiquement empêcher le contournement de frontière. Beaucoup de systèmes en production combinent les deux - un token bucket généreux par utilisateur pour le trafic API général, et une fenêtre glissante stricte sur des endpoints sensibles spécifiques.

