Toute API publique a besoin d'un rate limiter, et la plupart des équipes le découvrent de la même façon : un seul client avec une boucle de retry, ou un seul scraper, fait tomber le service pour tout le monde. Un limiteur plafonne ce que peut demander une clé, un utilisateur ou une IP : un acteur malveillant vous coûte alors un 429 au lieu d'une panne.
Les questions de conception sont toujours les mêmes : quel algorithme, où placer le limiteur, où vivent les compteurs, et que se passe-t-il quand leur stockage tombe. Cet article répond à chacune et se termine par un diagramme d'architecture d'un limiteur distribué devant une API.
Ce que doit faire le limiteur
- Décider en quelques millisecondes si une requête est autorisée, à chaque requête.
- Appliquer des règles différentes selon l'appelant : par clé d'API, par utilisateur, par IP, par route.
- Se comporter de la même façon quelle que soit l'instance qui traite la requête.
- Dire clairement aux clients quand ils sont limités et quand réessayer.
- Continuer à fonctionner, ou échouer de manière prévisible, quand son propre stockage a un problème.
C'est le troisième point qui en fait un problème de systèmes distribués. Une limite de 100 requêtes par minute ne veut rien dire si chacune des dix instances de la passerelle compte ses propres 100.
Cinq algorithmes
| Algorithme | Comment il compte | Force | Faiblesse |
|---|---|---|---|
| Token bucket | Un seau se remplit à débit fixe ; chaque requête prend un jeton | Autorise de courtes rafales jusqu'à la taille du seau | Deux paramètres à régler |
| Leaky bucket | Les requêtes s'écoulent d'une file à débit fixe | Débit de sortie régulier | Les rafales attendent dans la file et ajoutent de la latence |
| Compteur à fenêtre fixe | Un compteur par clé et par minute | Le moins cher : un incrément | Le double de la limite peut passer autour d'une frontière de fenêtre |
| Journal à fenêtre glissante | Stocke un horodatage par requête | Exact | La mémoire grandit avec le trafic |
| Compteur à fenêtre glissante | Pondère le compte de la fenêtre précédente selon le chevauchement | Presque exact, deux compteurs par clé | Une approximation |
Le problème de la fenêtre fixe se voit facilement : un client envoie 100 requêtes à 12:00:59 et 100 autres à 12:01:00, et les deux fenêtres les acceptent. Cela fait 200 requêtes en deux secondes pour une limite de 100 par minute.
Le token bucket est le choix habituel pour les API, car les rafales sont un comportement normal des clients. Une limite comme « 100 requêtes par minute, rafale de 20 » correspond directement à un débit de remplissage et à une taille de seau. Stripe a décrit ses limiteurs token bucket sur Redis dans son article sur la montée en charge d'une API avec des rate limiters, avec un limiteur séparé pour les requêtes simultanées.
Les compteurs à fenêtre glissante sont le moyen économique de supprimer le problème de frontière quand vous préférez une limite stricte par fenêtre.
Où placer le limiteur
Vous pouvez appliquer des limites à trois endroits, et beaucoup de systèmes en utilisent plusieurs :
- En bordure (CDN ou WAF). Des limites grossières par IP pour absorber les inondations avant qu'elles vous atteignent.
- Dans la passerelle API. Des limites par clé et par route, qui doivent savoir qui appelle. C'est là que vivent la plupart des limites produit.
- Dans un service. Des limites qui dépendent des données métier, comme « deux exports simultanés par compte ».
Une passerelle peut compter localement (chaque instance a ses compteurs) ou globalement (toutes les instances partagent un stockage). Les limites locales ne coûtent rien et suffisent pour une protection grossière ; divisez la limite par le nombre d'instances et acceptez l'erreur. Les limites produit que les clients paient exigent un stockage global. Envoy, par exemple, gère les deux : un filtre de limitation local et un mode global qui appelle un service de limitation externe adossé à Redis.
Des compteurs dans Redis
Redis est le choix courant pour des compteurs globaux : il est rapide, garde les compteurs en mémoire et fait expirer les clés tout seul. Le piège est la course entre la lecture d'un compteur et sa réécriture. Deux passerelles lisent 99, toutes deux autorisent la requête, toutes deux écrivent 100.
Gardez chaque décision atomique. Pour une fenêtre fixe, INCR suivi de EXPIRE sur une nouvelle clé suffit ; la documentation Redis d'INCR montre des modèles de limitation construits dessus. Pour un token bucket, exécutez un petit script Lua qui lit le seau, le remplit selon le temps écoulé, prend un jeton et réécrit le tout en une seule étape. Redis exécute un script sans y intercaler d'autres commandes, donc les requêtes concurrentes voient un seau cohérent.
-- KEYS[1] = bucket key, ARGV = rate per second, capacity, now (ms), cost
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local rate, cap, now, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4])
local tokens = tonumber(b[1]) or cap
local ts = tonumber(b[2]) or now
tokens = math.min(cap, tokens + (now - ts) / 1000 * rate)
local allowed = tokens >= cost
if allowed then tokens = tokens - cost end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(cap / rate * 1000))
return allowed and 1 or 0
Transmettez l'heure depuis l'appelant ou utilisez la commande Redis TIME ; mélanger les horloges de nombreuses passerelles est une source fréquente de résultats étranges. Si le même Redis sert aussi de cache, surveillez sa politique d'éviction : un seau évincé, c'est une limite remise à zéro. L'article sur les stratégies de cache couvre les réglages d'éviction.
Prévenir le client
Répondez à une requête limitée par 429 Too Many Requests, défini dans la RFC 6585, et ajoutez un en-tête Retry-After avec le nombre de secondes à attendre. Les bons clients ralentissent ; les autres reçoivent au moins un signal clair. Un brouillon de l'IETF, RateLimit header fields for HTTP, propose des en-têtes standard qui indiquent aux clients leur quota et leur budget restant avant qu'ils n'atteignent la limite ; au moment où nous écrivons (octobre 2026), c'est toujours un brouillon, donc considérez ses noms d'en-têtes comme provisoires.
Documentez les limites. Une limite que les clients découvrent en étant bloqués se transforme en ticket de support.
Quand Redis tombe
Décidez-le avant que cela arrive :
- Fail open. Laissez passer les requêtes quand le limiteur ne peut pas décider. L'API reste disponible, et vous perdez la protection pendant ce temps. C'est le choix habituel pour les limites produit.
- Fail closed. Rejetez les requêtes. C'est le bon choix pour les opérations coûteuses ou propices aux abus, comme les tentatives de connexion ou l'envoi de SMS.
- Revenir à des limites locales. Chaque passerelle applique une limite locale grossière jusqu'au retour du stockage.
Quel que soit votre choix, déclenchez une alerte. Un limiteur qui reste silencieusement en fail open pendant une semaine n'est plus un limiteur.
Le diagramme d'architecture
Contenu du diagramme
- API clients
- Load balancer
- API gateway
- Rate limit service
- Redis: counters
- Under the limit?
- Orders API
- 429 + Retry-After
- Metrics
- key + route
- atomic Lua script
- yes
- no
- allowed vs rejected
Comment le lire :
- De gauche à droite, c'est le chemin de la requête : les clients, le répartiteur de charge, la passerelle API et le service de limitation.
- Le service de limitation possède les règles et prend une décision par requête. Il garde les compteurs dans Redis et les met à jour par un script Lua atomique.
- Le nœud de décision envoie les requêtes autorisées à l'API Orders. Les autres reçoivent un 429 avec Retry-After et n'atteignent jamais le backend.
- Les métriques sont en pointillé, car elles sont remontées de façon asynchrone. Les rejets par règle sont la première chose à vérifier quand un client dit être bloqué.
- Le bloc Markdown liste les règles dans l'ordre où elles sont vérifiées, mode de défaillance compris.
Ce qu'il faut mettre sur le diagramme
- Les règles et leurs clés. « 100/min » ne veut rien dire sans « par clé d'API » ou « par IP ».
- Comptage local ou global. La différence décide si les limites tiennent d'une instance à l'autre.
- Le mode de défaillance. Fail open ou fail closed doit figurer sur le diagramme, pas seulement dans le code.
- Le chemin de rejet. Montrez où un 429 est produit, pour que personne ne suppose que le backend voit ces requêtes.
Pour la passerelle elle-même, le diagramme d'architecture d'une passerelle API couvre le routage, l'authentification et les autres filtres qui tournent à côté du limiteur. L'exemple de diagramme de conception de système montre comment un limiteur s'insère dans une conception plus large.
Le construire dans Archboard
- Connectez-vous avec Google ou GitHub et créez un diagramme.
- Depuis le pack Modèle C4, ajoutez une Personne pour les clients, puis depuis le pack Cloud un Répartiteur de charge cloud et une Passerelle API.
- Ajoutez deux nœuds Conteneur du pack Modèle C4 : le service de limitation et l'API Orders. Ajoutez Redis du pack Données sous le service de limitation.
- Ajoutez une Décision et un Début / Fin du pack Logigramme pour la branche accepter ou rejeter, et des Métriques du pack DevOps.
- Étiquetez les arêtes, dessinez l'arête des métriques en pointillé et ajoutez un bloc Markdown du pack Base avec les règles.
L'exemple utilise 10 nœuds de diagramme et reste dans les limites de l'offre Free.
FAQ
Quel algorithme de limitation choisir ? Le token bucket pour la plupart des API, car il autorise les courtes rafales que produisent les vrais clients. Utilisez un compteur à fenêtre glissante si vous avez besoin d'une limite stricte par fenêtre sans le coût d'un journal complet des requêtes.
Le rate limiter doit-il être un service à part ? Pas forcément. Un filtre de passerelle avec un Redis partagé suffit pour beaucoup de systèmes. Un service à part aide quand de nombreuses passerelles et de nombreux services partagent les mêmes règles.
Quel code de statut pour une requête limitée ? 429 Too Many Requests, avec un en-tête Retry-After. N'utilisez pas 503 : il dit aux clients que votre service est en panne, et non qu'ils ont trop demandé.
Comment limiter par utilisateur quand les requêtes passent par de nombreux serveurs ? Gardez les compteurs dans un stockage partagé comme Redis, indexés par utilisateur ou par clé d'API, et mettez-les à jour de façon atomique. Des compteurs locaux par serveur ne servent que de plafond approximatif.
Que se passe-t-il si le rate limiter lui-même tombe ? Choisissez à l'avance. Fail open pour les limites produit ordinaires, fail closed pour les opérations coûteuses ou propices aux abus, et une alerte dans les deux cas.