Vous avez activé la limitation de débit, vos graphiques se sont calmés, puis le support a explosé : « les clients ne peuvent pas se connecter ». Le trafic des bots a disparu. Les humains aussi. Bienvenue dans le piège classique de Nginx : activer la limitation est facile, la régler pour refléter le comportement réel des utilisateurs est étonnamment difficile.
Ce guide axé production s’adresse aux systèmes Ubuntu 24.04 exécutant Nginx. Nous choisirons des clés sensées, dimensionnerons les zones de mémoire partagée, gérerons les rafales sans récompenser les abuseurs, et diagnostiquerons les 429 sans conjectures. Vous repartirez avec des configurations défendables en revue de changement et un playbook que l’on peut exécuter à 02:00 les yeux à moitié ouverts.
Ce que fait réellement la limitation de débit Nginx (et ce qu’elle ne fait pas)
Nginx propose deux réglages principaux que l’on regroupe souvent sous « limitation de débit » :
limit_req: limite le taux de requêtes par clé (requêtes par seconde/minute). Pensez « à quelle vitesse ce client demande ? »limit_conn: limite le nombre de connexions simultanées par clé. Pensez « combien de sockets ce client garde ouverts ? »
limit_req sert pour la protection contre le bruteforce, l’équité API et la mitigation basique des balayages DDoS. limit_conn s’utilise pour les comportements de type slowloris, les clients hyper bavards, ou une erreur CDN/monitoring qui ouvre trop de connexions.
Ce que ni l’un ni l’autre ne font : ils ne distinguent pas les « vrais utilisateurs » des « faux ». Ils appliquent une politique. Si votre clé de politique est erronée ou que vos seuils ignorent le comportement web moderne (requêtes parallèles, retries, réseaux mobiles), vous bloquerez ceux qui paient votre salaire.
Note : la limitation n’est pas un WAF, ni une détection de bots, ni un substitut à des contrôles d’authentification. C’est un coupe-circuit pour des motifs de volume de requêtes.
Quelques faits et un peu d’histoire qui facilitent le réglage
Ce ne sont pas des anecdotes pour le plaisir. Ce sont les raisons pour lesquelles une règle « 5r/s par IP » fonctionne dans un environnement et explose dans un autre.
- Keepalive HTTP/1.1 a changé le sens de « connexions ». HTTP ancien ouvrait beaucoup de connexions courtes ; keepalive a permis à une seule connexion de transporter plusieurs requêtes. C’est pourquoi
limit_connpeut être inutile pour des inondations de requêtes et dévastateur pour WebSockets. - Multiplexage HTTP/2 a changé le sens de « parallèle ». Les navigateurs peuvent exécuter de nombreux flux concurrents sur une seule connexion TCP. Un client peut générer des rafales sans ouvrir plus de sockets, donc les contrôles de taux de requêtes prennent plus d’importance que le comptage de connexions.
- CDN et NAT opérateur regroupent des utilisateurs sur des IP partagées. Limiter par IP peut punir des bureaux entiers, des écoles, des hôtels et des pools de sortie mobile. Ce n’est pas un cas théorique ; c’est un mardi.
- La limitation Nginx utilise une zone de mémoire partagée. C’est rapide car en mémoire et partagée entre workers. C’est aussi borné, et quand elle est pleine, elle éjecte d’anciennes entrées. L’éviction change le comportement sous charge.
- Le modèle « seau fuyant » est ancien, mais toujours pertinent.
limit_reqest de type jetons/seau fuyant. C’est prévisible et peu coûteux comparé à des vérifications externes par requête. - 429 Too Many Requests est un contrat moderne. Ce n’est pas simplement « bloqué ». Beaucoup de clients vont réessayer ; certains vont reculer ; d’autres vont frapper plus fort. Renvoyer 429 ou 503 change la forme du trafic.
- Les retries sont devenus la norme. Clients mobiles, maillages de services et bibliothèques réessaient agressivement. Un « léger ralentissement » peut devenir une rafale de retries qui déclenche votre limiteur et empire la journée.
- Les attaquants se sont adaptés aux limites naïves depuis des années. Les botnets font tourner les IP ; les credential-stuffers répartissent les tentatives ; les scrapers utilisent des proxies résidentiels. Une seule limite globale par IP est surtout un ralentisseur.
Une citation à mettre dans chaque wiki d’astreinte : L’espoir n’est pas une stratégie.
— James Cameron. Courte, brutale, et vraie.
Principes : ne limitez pas des « utilisateurs », limitez des comportements
Si vous essayez de protéger « le site » avec un unique limiteur, vous serez soit trop strict pour les humains, soit trop généreux pour les abuseurs. Au lieu de cela :
- Protégez les actions coûteuses plus que les actions bon marché. Tentatives de connexion, réinitialisations de mot de passe, endpoints de recherche et pages dynamiques méritent des limites plus strictes. Les assets statiques peuvent être presque illimités (ou externalisés à un CDN).
- Préférez les clés d’identité aux clés réseau quand vous le pouvez. Limitez par clé API, ID utilisateur ou cookie de session pour le trafic authentifié. L’IP est un dernier recours pour le trafic « inconnu ».
- Laissez les rafales arriver, mais plafonnez l’abus soutenu. Les humains cliquent, les apps préchargent, les navigateurs ouvrent des connexions multiples, et le JS fait des requêtes en arrière-plan. Une petite marge de rafale évite les faux positifs.
- Échouez « poliment » pour les clients qui vont réessayer. Si vous renvoyez 429, incluez
Retry-Afterquand vous le pouvez. Vous façonnez le trafic, vous ne braquez pas juste la porte. - Rendez cela observable. Si vous ne pouvez pas répondre à « qui a été limité, sur quel endpoint, avec quelle clé, et était-ce attendu ? » alors vous pilotez à l’aveugle.
Blague 1/2 : La limitation de débit, c’est comme un videur de boîte—les bons arrêtent les bagarres, les mauvais expulsent le comptable parce qu’il avait « l’air suspect ».
Choisir la bonne clé : IP, utilisateur, jeton, ou quelque chose de plus intelligent
La clé est le cœur de votre limiteur. Elle définit « qui » est compté. La plupart des douleurs viennent du choix d’une clé qui ne reflète pas la réalité de votre trafic.
Option A: $binary_remote_addr (basée IP)
C’est le modèle par défaut car simple et ne nécessitant pas de coopération applicative.
- Avantages : Fonctionne pour le trafic anonyme ; peu coûteux ; pas de changement applicatif.
- Inconvénients : NAT et proxies regroupent plusieurs utilisateurs dans un même seau ; les IP qui tournent l’évitent ; derrière des LB vous pouvez limiter le LB lui-même si l’IP réelle n’est pas configurée.
Utilisez-la pour : tentatives bruteforce sur endpoints publics, protection de base pour zones non authentifiées, mitigation grossière pendant un incident.
Option B: identité authentifiée (cookie, revendication JWT, clé API)
Si vous avez une clé API dans l’en-tête comme X-API-Key, ou une revendication JWT que vous pouvez mapper en variable, vous pouvez limiter par client plutôt que par passerelle NAT.
- Avantages : Équitable ; résistant aux problèmes de NAT ; aligné sur la facturation/les frontières d’abus.
- Inconvénients : Nécessite un parsing fiable ; les chemins non authentifiés ont toujours besoin d’un secours ; des clés divulguées peuvent être abusées.
Option C: clés composites (IP + classe d’endpoint, ou IP + UA)
Les clés composites sont utiles quand vous ne pouvez pas faire confiance à l’identité et que vous voulez malgré tout séparer les comportements.
Exemple : limiter les tentatives de connexion par IP, mais aussi appliquer un plafond global bas pour une même IP qui cible de nombreux noms d’utilisateur. Nginx ne fera pas de corrélation cross-key pour vous, mais vous pouvez définir différentes zones par emplacement et choix de clé.
Ce que je recommande réellement en production
Utilisez deux couches :
- Couche anonyme : limite basée IP pour les endpoints sensibles (login, réinitialisation, recherche). Rafale modeste. Taux soutenu strict.
- Couche authentifiée : limite par client/token pour les APIs. Rafale plus élevée. Taux soutenu plus élevé. Limites séparées par plan si possible.
Et si vous êtes derrière un proxy inverse, corrigez l’IP réelle en premier. Sinon vous limitez un load balancer et appelez cela « sécurité ».
Dimensionner les zones mémoire partagées et comprendre l’éviction
Nginx stocke l’état du limiteur dans une zone de mémoire partagée définie par limit_req_zone. Cette zone a une taille fixe. Lorsqu’elle se remplit, les anciennes entrées sont évincées. L’éviction provoque deux sortes de bizarrerie :
- Sous-limitation : les clients abusifs font tourner suffisamment de clés uniques pour s’éjecter eux-mêmes de la zone, oubliant ainsi leur historique.
- Surrésolution d’innocents : moins fréquent, mais quand une zone thrash, vous pouvez voir un comportement instable où certains clients passent parfois et reçoivent parfois des 429, selon que leur entrée est en mémoire.
La documentation Nginx suggère souvent une mémoire approximative par entrée, mais en pratique vous dimensionnez selon le nombre de clés uniques attendues pendant les pics. Pour une limitation basée IP sur un site public, les clés uniques peuvent être étonnamment nombreuses.
Heuristique pratique de dimensionnement
Commencez par :
- 10m pour les petits sites ou endpoints dédiés
- 50m–200m pour des bords publics à fort trafic
Puis mesurez. Si vous exécutez plusieurs zones (recommandé), chaque zone nécessite sa propre mémoire.
Rafales, nodelay, et pourquoi « lisse » n’est pas toujours bienveillant
La plupart des faux positifs surviennent à cause des rafales. Les vrais navigateurs et apps mobiles ne font pas une requête par seconde comme des robots polis. Ils font :
- Charger le HTML
- Demander immédiatement CSS, JS, images, polices
- Exécuter des appels API (parfois plusieurs) une fois le JS chargé
- Réessayer rapidement une requête échouée
limit_req supporte le burst et le nodelay :
burst=Npermet N requêtes excessives de faire la queue (ou d’être retardées), lissant un pic.nodelayfait passer les rafales immédiatement jusqu’à la limite de burst (pas de délai) ; au-delà, les requêtes sont rejetées.
Le compromis :
- burst sans nodelay : plus clément pour les backends, mais les utilisateurs ressentent de la latence et peuvent réessayer, ce qui peut amplifier la charge.
- burst avec nodelay : meilleure UX pour de petites rafales, mais peut envoyer un coup plus dur à votre upstream lors d’attaques.
Pour les endpoints de login, je préfère souvent pas de nodelay (retarder les abuseurs) mais une petite rafale pour les doubles-clics légitimes. Pour les endpoints API où les clients expirent et réessayeront, je préfère souvent nodelay avec un burst modéré, afin que les micro-rafales normales ne deviennent pas des latences longues.
Des limites différentes selon les endpoints (les connexions ne sont pas du CSS)
Cessez d’appliquer un unique limiteur à / et d’appeler ça terminé. Vous voulez des politiques par classe :
- Login / auth : strict par IP, très strict par nom d’utilisateur si l’application le supporte (Nginx seul ne peut pas le faire proprement, mais vous pouvez clefier sur un paramètre de login avec des modules supplémentaires ; souvent c’est fait dans l’app). Ajoutez des exemptions par session prudemment.
- Réinitialisation de mot de passe / OTP : strict. L’abus ici coûte de l’argent et de la réputation.
- Recherche / requêtes coûteuses : strict par IP ou par token, car les bots adorent ces endpoints.
- API générale : par clé API/client. Différents paliers si possible.
- Assets statiques : généralement pas de
limit_req; utilisez le cache CDN ou laissez-les circuler. Si vous devez, mettez une limite très élevée et concentrez-vous sur le contrôle des connexions. - WebSocket / SSE : ne pas utiliser le rate limiting après l’upgrade ; envisagez des limites de connexions keyées par IP ou token.
Derrière un load balancer : IP réelle, frontières de confiance, et usurpation
Sur Ubuntu 24.04, Nginx se situe fréquemment derrière un load balancer cloud, un contrôleur d’ingress, un CDN ou un service mesh. Si vous utilisez bêtement $remote_addr, vous pourriez limiter l’adresse du proxy. Cela signifie :
- Un client bruyant peut brider tout le monde.
- Ou vous réglez des limites si hautes qu’elles sont inefficaces, et les attaquants traversent.
Corrigez cela avec le module Real IP, mais soyez strict sur la confiance. N’acceptez X-Forwarded-For (ou le protocole PROXY) que des plages IP de proxies connues. Si vous faites confiance à tout l’internet, n’importe quel client peut forger un X-Forwarded-For et obtenir des identités fraîches à l’infini.
Journalisation et observabilité : rendre les 429 exploitables
Le format d’accès par défaut ne vous dira pas pourquoi une requête a été limitée. Vous avez besoin de :
- la clé utilisée pour la limitation (ou au moins l’IP client après le traitement real-ip)
- le temps de requête et le temps upstream
- le code de statut et les octets envoyés
- un identifiant de requête pour la corrélation
- quel limiteur a été déclenché
Nginx expose $limit_req_status qui est extrêmement utile. Logger ce champ. Si c’est « REJECTED », vous savez que c’est le limiteur. Si c’est « PASSED » mais que les clients se plaignent encore, votre goulot est ailleurs.
Tâches pratiques : commandes, sorties et décisions (12+)
Voici ce que j’exécute réellement sur Ubuntu quand quelqu’un dit « la limitation bloque des utilisateurs réels » ou « le site est attaqué ». Chaque tâche inclut ce que la sortie signifie et la décision associée.
Task 1: Confirm Nginx version and build modules
cr0x@server:~$ nginx -V
nginx version: nginx/1.24.0 (Ubuntu)
built with OpenSSL 3.0.13 30 Jan 2024
configure arguments: ... --with-http_realip_module --with-http_limit_req_module --with-http_limit_conn_module ...
Ce que cela signifie : Vous avez les modules Real IP, limit_req et limit_conn compilés (courant pour les paquets Ubuntu).
Décision : Si --with-http_realip_module est absent et que vous êtes derrière un proxy, arrêtez-vous et corrigez votre stratégie de packaging (utilisez le paquet de la distribution, ou recompilez). Sans real IP, votre sélection de clé peut être vaine.
Task 2: Validate the active config (not the one you think is active)
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Ce que cela signifie : La syntaxe est valide.
Décision : Si cela échoue, ne rechargez pas. Corrigez la config d’abord. Des changements de limitation qui ne se chargent pas deviennent des légendes « on a changé quelque chose » sans effet réel.
Task 3: Dump the full loaded configuration to see includes and overrides
cr0x@server:~$ sudo nginx -T | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
...
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
Ce que cela signifie : Vous pouvez localiser où limit_req et limit_req_zone sont définis.
Décision : Si vous trouvez plusieurs limiteurs en conflit dans conf.d et sites-enabled, consolidez-les. Des limites fragmentées donnent « marche en staging » et « bloque en prod ».
Task 4: Check whether you are rate limiting the load balancer (real IP sanity)
cr0x@server:~$ sudo tail -n 5 /var/log/nginx/access.log
10.10.5.12 - - [30/Dec/2025:10:10:12 +0000] "GET /login HTTP/2.0" 200 512 "-" "Mozilla/5.0 ..."
10.10.5.12 - - [30/Dec/2025:10:10:12 +0000] "GET /api/me HTTP/2.0" 429 169 "-" "Mozilla/5.0 ..."
10.10.5.12 - - [30/Dec/2025:10:10:13 +0000] "GET /api/me HTTP/2.0" 429 169 "-" "Mozilla/5.0 ..."
Ce que cela signifie : Si tous les clients apparaissent avec la même adresse RFC1918 (comme 10.10.5.12), c’est probablement votre proxy/LB.
Décision : Corrigez Real IP avant de toucher aux seuils. Sinon vous ajusterez pour le mauvais « client » et bloquerez tout le monde en même temps.
Task 5: Verify Real IP is configured and trust is narrow
cr0x@server:~$ sudo nginx -T | grep -E 'real_ip_header|set_real_ip_from|real_ip_recursive'
real_ip_header X-Forwarded-For;
set_real_ip_from 10.10.0.0/16;
set_real_ip_from 192.0.2.10;
real_ip_recursive on;
Ce que cela signifie : Nginx remplacera $remote_addr en utilisant XFF, mais seulement quand la requête vient de plages proxy de confiance.
Décision : Si vous voyez set_real_ip_from 0.0.0.0/0;, vous avez effectivement invité l’usurpation. Corrigez cela immédiatement. Votre limitation peut être contournée par un en-tête.
Task 6: Inspect error log for limit_req signals
cr0x@server:~$ sudo grep -E 'limiting requests|limit_req' /var/log/nginx/error.log | tail -n 5
2025/12/30 10:10:13 [error] 12410#12410: *991 limiting requests, excess: 5.610 by zone "api_per_ip", client: 203.0.113.55, server: example, request: "GET /api/me HTTP/2.0", host: "example"
Ce que cela signifie : Nginx rejette à cause de la zone api_per_ip, et affiche l’« excess » calculé. C’est de l’or pour le réglage.
Décision : Si des humains sont bloqués, décidez si la clé est mauvaise (NAT/proxy), le taux trop bas, ou le burst trop petit.
Task 7: Add a log format that records limiter status (and check it works)
cr0x@server:~$ sudo grep -R "limit_req_status" -n /etc/nginx | head
/etc/nginx/nginx.conf:35:log_format main_ext '$remote_addr $request_id $status $request $limit_req_status';
Ce que cela signifie : Vos logs enregistrent désormais si le limiteur a passé, retardé ou rejeté une requête.
Décision : Si vous ne voyez pas $limit_req_status par requête, vous diagnostiquerez mal « 429 dû à l’upstream » vs « 429 dû au limiteur ». Ajoutez-le avant de régler.
Task 8: Confirm what’s returning 429 (Nginx vs upstream)
cr0x@server:~$ curl -s -D - -o /dev/null https://example/api/me | sed -n '1,12p'
HTTP/2 429
server: nginx
date: Tue, 30 Dec 2025 10:12:10 GMT
content-type: text/html
content-length: 169
Ce que cela signifie : La réponse vient de Nginx (voir server: nginx). Si votre upstream renvoie aussi des 429, vous aurez besoin d’en-têtes/champs de log supplémentaires pour distinguer.
Décision : Si c’est Nginx, réglez Nginx. Si c’est l’upstream, ne perdez pas de temps à ajuster limit_req.
Task 9: Measure current request rates per client IP quickly
cr0x@server:~$ sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
8421 203.0.113.55
2210 198.51.100.27
911 203.0.113.101
455 192.0.2.44
Ce que cela signifie : Quelles IP sont les plus actives dans vos logs pour la fenêtre échantillonnée (ce n’est pas un vrai taux, mais une carte de chaleur rapide).
Décision : Si quelques IP dominent, les limites par IP seront probablement efficaces. Si vous voyez beaucoup d’IP avec de faibles comptes, vous êtes face à un trafic distribué et les limites par IP aideront peu.
Task 10: Check active connections and who holds them
cr0x@server:~$ sudo ss -Htn state established '( sport = :443 )' | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -nr | head
120 203.0.113.55
48 198.51.100.27
11 192.0.2.44
Ce que cela signifie : Clients ayant beaucoup de connexions TCP établies vers le port 443.
Décision : Si un client détient des centaines/milliers de connexions, limit_conn est un meilleur outil que limit_req. Si les connexions sont faibles mais les requêtes élevées, concentrez-vous sur limit_req et le caching.
Task 11: Check whether HTTP/2 is enabled (affects burst behavior)
cr0x@server:~$ sudo nginx -T | grep -R "listen 443" -n /etc/nginx/sites-enabled | head
/etc/nginx/sites-enabled/example.conf:12: listen 443 ssl http2;
Ce que cela signifie : Le multiplexage HTTP/2 est actif.
Décision : Attendez-vous à des patterns « en rafales » venant de navigateurs normaux. Augmentez le burst pour les endpoints frontaux, ou restreignez la limitation aux chemins coûteux.
Task 12: Track 429s over time from logs (cheap trend view)
cr0x@server:~$ sudo awk '$9==429 {c++} END{print c+0}' /var/log/nginx/access.log
317
Ce que cela signifie : Nombre de réponses 429 dans le fichier de log courant.
Décision : Si ce nombre est significatif, vous devez classifier : s’agit-il de bots attendus ou de clients légitimes ? Ajoutez ensuite une répartition par endpoint.
Task 13: Break down 429s by path to find what you’re actually blocking
cr0x@server:~$ sudo awk '$9==429 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
210 /api/me
61 /login
24 /search
12 /password/reset
Ce que cela signifie : Quels endpoints sont limités.
Décision : Si /api/me est limité et que votre SPA l’appelle à chaque chargement de page, votre baseline est trop stricte ou votre clé est mauvaise (NAT/proxy). Si /login est fortement limité, cela peut être correct (credential stuffing) mais vérifiez les plaintes clients.
Task 14: Confirm zone configuration and rates (find the policy you’re enforcing)
cr0x@server:~$ sudo nginx -T | grep -E 'limit_req_zone|limit_req[^_]' -n | head -n 30
45: limit_req_zone $binary_remote_addr zone=login_per_ip:20m rate=5r/m;
46: limit_req_zone $binary_remote_addr zone=api_per_ip:50m rate=10r/s;
112: limit_req zone=login_per_ip burst=3;
166: limit_req zone=api_per_ip burst=20 nodelay;
Ce que cela signifie : Votre login est limité à 5 requêtes par minute par IP. Votre API est à 10 requêtes par seconde par IP. Ce sont des politiques très différentes.
Décision : Décidez si ces chiffres correspondent à la réalité. 10r/s par IP peut être trop bas si des clients corporatifs derrière NAT existent. 5r/m sur le login peut être trop bas si la page de login déclenche plusieurs appels liés à l’authent.
Task 15: Reload safely and confirm workers pick it up
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ systemctl status nginx --no-pager | sed -n '1,12p'
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-30 09:55:01 UTC; 18min ago
Docs: man:nginx(8)
Ce que cela signifie : Le reload a réussi et Nginx est resté en fonctionnement.
Décision : Si vous voyez des reloads échoués, arrêtez d’itérer. Corrigez l’hygiène de configuration d’abord. Tuner la limitation de débit dans une chaîne de déploiement cassée, c’est de l’art plastique.
Playbook de diagnostic rapide
Quand les 429 augmentent ou que les utilisateurs se plaignent, ne flânez pas. Faites ceci dans l’ordre. Le but est de retrouver le goulot (politique, clé ou capacité) en moins de 10 minutes.
First: confirm who is generating the 429 and why
- Vérifiez les logs d’accès pour le statut 429 et les chemins principaux (Task 12, Task 13).
- Vérifiez le log d’erreur pour les messages
limiting requests(Task 6).- Si le log d’erreur montre des rejets du limiteur : c’est une politique Nginx.
- Sinon : le 429 peut venir de l’application upstream ou d’un autre gateway.
- Vérifiez les en-têtes via curl (Task 8) pour voir si Nginx sert le 429.
Second: verify the key is sane in your topology
- Regardez les IP clients dans les logs (Task 4).
- Si vous voyez des IP de LB : la config real IP est manquante/erronée.
- Si vous voyez un petit ensemble d’IP NATées : la limitation par IP peut punir de nombreux utilisateurs.
- Vérifiez les frontières de confiance Real IP (Task 5). Assurez-vous de ne faire confiance qu’à vos proxies.
Third: decide if thresholds or burst are wrong (or if you need endpoint-specific limits)
- Inspectez les zones et taux actuels (Task 14).
- Vérifiez l’usage HTTP/2 (Task 11). Si activé, augmentez le burst pour les endpoints face au navigateur.
- Vérifiez si le problème porte sur les connexions ou les requêtes (Task 10). Utilisez
limit_connpour les hoardeurs de connexions.
Fourth: verify it’s not a capacity problem masquerading as rate limiting
Quand les upstreams ralentissent, les retries augmentent, ce qui augmente le taux de requêtes, ce qui déclenche votre limiteur. Le limiteur n’est pas « faux » ; il met en évidence un vrai problème de capacité.
Utilisez vos métriques existantes, mais sur la machine vous pouvez au moins logger les champs de timing upstream et repérer les upstreams lents dans les logs d’accès. Si les temps upstream sont élevés, ajustez d’abord l’upstream ou ajoutez du cache, puis revenez au limiteur.
Trois mini-récits d’entreprise venus des opérations
Mini-story 1: The incident caused by a wrong assumption (NAT is not “rare”)
Une société B2B de taille moyenne a déployé limit_req avec une règle propre : 5 requêtes par seconde par IP sur /api/. En staging tout semblait parfait. En production, tout allait bien jusqu’au lundi matin en Amérique du Nord.
Les tickets de support ont afflué : « L’app tourne en boucle », « Le tableau de bord ne charge pas », « 429 aléatoires ». L’équipe a regardé les tableaux et a vu une légère hausse de trafic—rien de dramatique. Le rollback n’a rien changé.
Sur la machine edge, les logs d’accès racontaient l’histoire : plusieurs clients payants apparaissaient depuis une poignée d’IP. Ce n’étaient pas des « utilisateurs ». C’étaient des passerelles de sortie d’entreprise et des pools NAT opérateur. Un seul bureau avec des centaines d’employés partageant une IP de sortie pouvait désormais « dépenser » seulement 5 requêtes par seconde pour toute l’application.
Le correctif immédiat fut moche mais efficace : augmenter substantiellement la limite par IP et n’ajouter des règles strictes qu’aux endpoints sensibles (login, réinitialisation). La vraie correction fut une meilleure clé : par token API pour les appels API authentifiés, et par cookie de session pour le trafic navigateur quand c’était possible.
Après l’incident, ils ont ajouté à leur checklist pré-déploiement : « Montrer la distribution IP uniques vs IDs authentifiés pendant le pic ». C’est devenu une partie routinière de la planification de capacité, pas un détail après coup.
Mini-story 2: The optimization that backfired (nodelay everywhere)
Une autre entreprise avait un client bruyant. Quelqu’un a suggéré : « Utilisez burst avec nodelay pour que les vrais utilisateurs ne ressentent pas la limitation. » Ils l’ont appliqué largement : tous les endpoints API, tous les clients, rafales généreuses, nodelay activé.
L’expérience utilisateur s’est améliorée dans le happy path. Puis un partenaire d’intégration a mal configuré sa politique de retry. Leur client a commencé à frapper des timeouts et à réessayer agressivement en parallèle. Avec nodelay et un gros burst, Nginx a laissé passer de grosses micro-rafales vers les services upstream, déjà sous pression.
Les upstreams ont commencé à tomber, ce qui a augmenté les timeouts, ce qui a augmenté les retries, ce qui a augmenté les rafales. Une boucle de rétroaction sous une apparence UX conviviale. Le monitoring montrait un Nginx « ok » et CPU stable, mais les erreurs upstream et la latence ont explosé.
La solution a été une politique plus nuancée : garder nodelay pour quelques endpoints en lecture rapide, mais retirer nodelay pour les endpoints coûteux et réduire les tailles de burst. Ils ont aussi introduit une protection de file d’attente en amont et de meilleurs timeouts/retries dans le SDK client.
La morale n’était pas « nodelay est mauvais ». La morale était que le lissage et l’UX doivent être alignés avec la capacité backend, pas avec de l’optimisme.
Mini-story 3: The boring but correct practice that saved the day (log the limiter status)
Une équipe fintech avait l’habitude, en apparence banale lors des revues de code : chaque fois qu’ils ajoutaient un limiteur, ils ajoutaient aussi des champs de log pour $request_id, $limit_req_status, et la clé utilisée (hachée si nécessaire). Aucune exception.
Un soir, ils ont constaté un pic de 429 et une chute de conversion. La suspicion initiale était une attaque de bots. Ce n’était pas le cas. Les logs montraient que la plupart des 429 étaient « PASSED » et que le code de réponse upstream était 429—ce qui signifiait que l’application faisait elle-même de la limitation parce qu’un fournisseur de paiement en aval renvoyait des erreurs et que leur coupe-circuit se déclenchait.
Grâce aux logs explicites au bord, ils n’ont pas perdu une heure à desserrer les limites Nginx et à inviter le vrai abus. Ils ont corrigé la logique de basculement du fournisseur de paiement, ajusté les fenêtres de retry, et laissé la politique edge intacte.
Cet incident n’est pas devenu un festival de blâme entre équipes. C’est devenu une réparation en 40 minutes avec une timeline claire. Le logging ennuyeux a gagné contre les conjectures dramatiques.
Erreurs courantes : symptômes → cause racine → correction
1) « Tous les utilisateurs reçoivent des 429 en même temps »
Symptômes : 429 généralisés et soudains ; utilisateurs dans plusieurs régions affectés ; les logs montrent la même IP client.
Cause racine : vous limitez l’IP du load balancer/proxy parce que Real IP n’est pas configuré (ou mal configuré).
Correction : configurez real_ip_header et rétrécissez set_real_ip_from aux plages IP de proxy de confiance. Ensuite utilisez $binary_remote_addr (après traitement real IP) comme clé.
2) « Les clients corporate se plaignent, les utilisateurs domestiques vont bien »
Symptômes : les réseaux de bureau voient des erreurs ; les mobiles et résidences non ; la liste d’IP montre quelques IP avec beaucoup de trafic.
Cause racine : les limites par IP punissent les réseaux NATés et les egress VPN.
Correction : pour les endpoints authentifiés, limiter par clé API ou session. Pour l’anonyme, augmentez les limites par IP et concentrez les limites strictes sur les endpoints coûteux spécifiques.
3) « Le login est cassé, mais seulement parfois »
Symptômes : échecs de connexion intermittents ; les utilisateurs réussissent après un délai ; pics durant des campagnes marketing.
Cause racine : le flow de login fait plusieurs appels (fetch CSRF, préflight MFA, télémétrie) et déclenche un limiteur strict avec un burst trop petit.
Correction : appliquez la limitation au POST des identifiants, pas à chaque requête sous /login. Augmentez un peu le burst ; envisagez de retarder (pas de nodelay) au lieu de rejeter pour de faibles excès.
4) « Nous avons activé la limitation, mais les attaques font toujours mal »
Symptômes : la charge backend reste élevée ; les logs du limiteur montrent peu de rejets ; beaucoup d’IP sources.
Cause racine : trafic distribué (botnets, proxies résidentiels) rendant les limites par IP inefficaces ; ou la zone est trop petite et thrash.
Correction : ajoutez des limites basées identité (clé API, token), des politiques spécifiques par endpoint, du caching et des protections upstream. Augmentez la taille des zones et surveillez l’effet d’éviction.
5) « Après ajustement, l’upstream a commencé à échouer davantage »
Symptômes : moins de 429, mais plus de 5xx upstream ; latence en hausse.
Cause racine : vous avez assoupli les limites sans capacité ; ou vous avez activé nodelay avec un gros burst et transmis les pics à l’upstream.
Correction : réintroduisez le lissage (supprimez nodelay) pour les endpoints coûteux ; fixez des bursts raisonnables ; ajustez timeouts upstream ; ajoutez cache et mise en file.
6) « La limitation est contournée »
Symptômes : le limiteur semble inefficace ; les attaquants apparaissent avec beaucoup d’IP ; les logs montrent des valeurs XFF étranges.
Cause racine : confiance à X-Forwarded-For depuis des sources non fiables (set_real_ip_from 0.0.0.0/0 ou équivalent).
Correction : ne faites confiance qu’aux proxies connus ; envisagez le protocole PROXY si supporté ; validez que l’IP client ne change que quand la requête vient d’un réseau proxy de confiance.
Listes de vérification / plan étape par étape
Ceci est la séquence qui tend à fonctionner en environnements réels sans transformer votre edge en machine à sous.
Plan étape par étape : mettre en place la limitation sans bloquer les humains
- Fixez l’identité client au bord.
- Derrière un proxy ? Configurez Real IP avec une confiance étroite.
- Décidez ce que « client » signifie : IP pour l’anonyme, token pour l’authentifié.
- Classifiez les endpoints.
- Endpoints d’auth (login, reset) = stricts
- Endpoints coûteux (recherche, rapports) = plutôt stricts
- Endpoints en lecture bon marché = modérés
- Assets statiques = généralement pas de rate limit
- Créez des zones séparées par classe.
- Ne réutilisez pas une zone pour tout. Vous masquerez quel comportement est abusif.
- Définissez des taux initiaux avec biais vers moins de faux positifs.
- Commencez plus haut que vous ne le pensez, puis réduisez selon l’abus observé.
- Utilisez le burst pour protéger les humains contre les clients en rafale.
- Logger le statut du limiteur et les request IDs.
- Si vous ne le loggez pas, vous ne pouvez pas régler.
- Déployez progressivement.
- Appliquez d’abord à une classe d’endpoints (login est un bon candidat).
- Surveillez le taux de 429, la conversion et les logs d’erreur.
- Choisissez votre comportement de réponse.
- Utilisez 429 pour le throttling d’équité.
- Envisagez de retarder (pas de
nodelay) pour le bruteforce afin de perdre du temps aux attaquants.
- Testez avec une forme de trafic réaliste.
- Navigateurs (HTTP/2) et apps mobiles sont en rafales.
- Incluez les retries dans les tests de charge, car la production en aura.
Une configuration de base solide (opinionnée)
Ce modèle suppose :
- Vous êtes derrière un LB de confiance dans
10.10.0.0/16 - Vous souhaitez une protection stricte des logins par IP
- Vous voulez une protection API par clé API quand présente, sinon par IP
- Vous voulez des logs qui expliquent ce qui s’est passé
cr0x@server:~$ sudo sed -n '1,220p' /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main_ext '$remote_addr $request_id $time_local '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'lrs=$limit_req_status '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
real_ip_header X-Forwarded-For;
set_real_ip_from 10.10.0.0/16;
real_ip_recursive on;
# Key selection for APIs: API key if present, else IP.
map $http_x_api_key $api_limit_key {
default $http_x_api_key;
"" $binary_remote_addr;
}
# Whitelist internal monitoring and office VPN (example).
map $binary_remote_addr $is_whitelisted {
default 0;
192.0.2.44 1;
198.51.100.77 1;
}
# Zones: size according to expected unique keys.
limit_req_zone $binary_remote_addr zone=login_per_ip:20m rate=5r/m;
limit_req_zone $api_limit_key zone=api_per_key:100m rate=20r/s;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
cr0x@server:~$ sudo sed -n '1,220p' /etc/nginx/sites-enabled/example.conf
server {
listen 443 ssl http2;
server_name example;
# Default: do not rate limit everything. Target specific locations.
location = /login {
if ($is_whitelisted) { break; }
limit_req zone=login_per_ip burst=6;
proxy_pass http://app_upstream;
}
location = /password/reset {
if ($is_whitelisted) { break; }
limit_req zone=login_per_ip burst=3;
proxy_pass http://app_upstream;
}
location ^~ /api/ {
if ($is_whitelisted) { break; }
limit_req zone=api_per_key burst=40 nodelay;
proxy_pass http://app_upstream;
}
location / {
proxy_pass http://app_upstream;
}
}
Pourquoi cette base fonctionne :
- Le login est throttlé par IP à une échelle humaine (par minute), autorisant de petites rafales.
- L’API est throttlée par clé client quand disponible, ce qui évite la douleur du NAT.
- La limitation est ciblée ; vous ne cassez pas le chargement des assets ou la navigation basique.
- Les logs capturent le statut du limiteur pour que vous puissiez régler avec des preuves.
Blague 2/2 : Si vous limitez vos propres health checks, félicitations — vous venez d’inventer le « self-care » des serveurs, et ils le prendront au pied de la lettre.
FAQ
1) Quelle est la différence entre limit_req et limit_conn ?
limit_req limite le taux de requêtes (r/s ou r/m). limit_conn limite les connexions concurrentes. Utilisez les limites de requêtes pour les inondations et l’équité ; utilisez les limites de connexions pour le hoarding de connexions et les flux longue durée.
2) Dois-je limiter globalement à location / ?
Presque jamais. Vous punirez le comportement normal des navigateurs et des retries. Limitez les endpoints coûteux ou sensibles, puis ajoutez une baseline douce uniquement si vous en avez vraiment besoin.
3) Pourquoi les vrais utilisateurs se font bloquer alors que les bots non ?
Parce que votre clé est mauvaise (NAT/proxy), vos seuils sont trop bas pour les patterns de rafale normaux, ou les bots sont distribués sur beaucoup d’IP tandis que vos utilisateurs sont concentrés derrière une sortie partagée.
4) $binary_remote_addr est-il meilleur que $remote_addr ?
Pour les clés du limiteur, oui. C’est une représentation binaire compacte et recommandée pour l’efficacité mémoire partagée. Mais cela n’aide que si Real IP est correctement configuré derrière des proxies.
5) Quand dois-je utiliser nodelay ?
Utilisez-le lorsque vous voulez autoriser de brèves rafales sans ajouter de latence et que votre upstream peut accepter de courts pics. Évitez-le sur les endpoints coûteux ou quand l’upstream est déjà instable.
6) Comment éviter de punir les utilisateurs derrière un NAT ?
Limitez le trafic authentifié par identité client (clé API, token, session) et gardez les limites par IP surtout pour les endpoints anonymes sensibles. Évitez aussi des seuils très bas par IP sur les APIs générales.
7) Puis-je « whitelist » certains clients en toute sécurité ?
Oui, mais gardez cela restreint et auditable (IPs de monitoring, egress VPN, IPs partenaires spécifiques). Les whitelists ont tendance à grandir et devenir un risque si elles sont mal gérées.
8) Comment savoir si la zone du limiteur est trop petite ?
Vous verrez une limitation inconsistante sous forte cardinalité de clés uniques et des patterns qui n’ont pas de sens. Concrètement : si vous avez un endpoint public avec une petite zone, augmentez-la et observez si le comportement du limiteur se stabilise.
9) Renvoyer 429 est-il toujours le bon choix ?
Pour le throttling d’équité, oui. Pour la dissuasion brute-force, retarder (pas de nodelay) peut être plus efficace. Pour la protection en cas d’effondrement, renvoyer 503 avec des mécanismes de backoff peut aussi avoir du sens, mais faites-le intentionnellement.
10) Nginx peut-il faire une limitation par nom d’utilisateur sur les tentatives de connexion ?
Pas proprement avec les variables stock ; le nom d’utilisateur est généralement dans le corps POST. Faites la limitation par IP à la frontière Nginx, et implémentez la limitation par nom d’utilisateur et par compte dans la couche applicative.
Conclusion : étapes pratiques suivantes
Si vous voulez une limitation qui ne plie pas les vrais utilisateurs, faites trois choses avant de toucher un seul nombre :
- Fixez l’identité au bord. IP réelle derrière les proxies, et clés d’identité pour les APIs authentifiées.
- Limitez par comportement, pas par site. Zones et politiques séparées pour login/reset/recherche/API. Laissez le reste tranquille sauf preuve du contraire.
- Rendez les 429 débogables. Loggez
$limit_req_status, ajoutez des request IDs, et vérifiez les logs d’erreur pour les messages du limiteur.
Puis réglez comme un adulte : mesurez, changez une chose, rechargez en sécurité, et observez les effets. Votre objectif n’est pas « moins de requêtes ». Votre objectif est « moins de mauvaises requêtes, les bons utilisateurs inchangés ». C’est tout le travail.