Rate Limiting et Throttling en Go : comment protéger son API sans perdre la tête
\n\nImaginez : vous avez écrit l'API parfaite en Go. Elle est rapide, traite les requêtes en millisecondes, et soudain — boum ! — un déluge de requêtes s'abat sur le serveur. C'est peut-être un bot, un bug chez le client, ou simplement le buzz autour de votre produit. Sans protection, le serveur s'effondre, les utilisateurs s'en vont, et vous passez la nuit à analyser les logs.
\n\nÇa vous parle ? Alors le rate limiting (limitation de débit) et le throttling (étranglement) sont vos nouveaux meilleurs amis. Dans cet article sur pythonlib.ru, nous allons voir comment les implémenter en Go — un langage conçu pour les systèmes à forte charge. Pas de blabla, que du code concret et des cas réels.
\n\nRate Limiting vs Throttling : quelle différence ?
\n\nCommençons par clarifier les termes. Beaucoup confondent ces concepts, mais il y a une différence.
\n\n- \n
- Rate Limiting — c'est une limitation stricte : « Pas plus de 100 requêtes par minute ». Si le seuil est dépassé, le serveur répond 429 Too Many Requests. C'est clair et net. \n
- Throttling — c'est une régulation douce : « On va te ralentir, mais pas te bloquer ». Par exemple, au lieu de répondre immédiatement à toutes les requêtes, on les met en file d'attente et on les traite à une vitesse fixe. \n
En Go, les deux approches s'implémentent élégamment grâce aux goroutines et aux canaux. Regardons des exemples concrets.
\n\nRate limiter simple avec des canaux
\n\nLa façon la plus basique de limiter le débit est d'utiliser un ticker du package time. C'est comme un métronome : chaque « tic » autorise une requête.
package main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc main() {\n // Crée un canal qui enverra des jetons toutes les 200 ms\n limiter := time.Tick(200 * time.Millisecond)\n\n // Simule 10 requêtes\n for i := 1; i <= 10; i++ {\n <-limiter // Attend l'autorisation\n fmt.Printf("Requête %d traitée à %s\\n", i, time.Now().Format("15:04:05.000"))\n }\n}\n\nQue se passe-t-il ici ? time.Tick crée un canal qui envoie une valeur toutes les 200 millisecondes. Notre goroutine se bloque sur <-limiter jusqu'à ce qu'elle reçoive un jeton. Résultat : exactement 5 requêtes par seconde, sans exception.
Inconvénient : s'il y a moins de requêtes que de tics, on attend simplement. Pas efficace ? Oui. Mais pour des scénarios simples, c'est parfait.
\n\nBurst limiting avec un buffer
\n\nEt si on a besoin de laisser passer des « pics » de trafic ? Par exemple, autoriser 10 requêtes instantanément, puis limiter à 5 par seconde. Pour cela, on utilise un canal bufferisé — le classique Token Bucket.
\n\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc main() {\n // Canal bufferisé de 5 jetons\n tokens := make(chan struct{}, 5)\n\n // Goroutine remplisseuse : ajoute un jeton toutes les 200 ms\n go func() {\n ticker := time.NewTicker(200 * time.Millisecond)\n defer ticker.Stop()\n for range ticker.C {\n select {\n case tokens <- struct{}{}:\n default:\n // Canal plein — on ne fait rien\n }\n }\n }()\n\n // Simulation de 15 requêtes\n for i := 1; i <= 15; i++ {\n <-tokens // Prend un jeton (attend si vide)\n fmt.Printf("Requête %d traitée à %s\\n", i, time.Now().Format("15:04:05.000"))\n time.Sleep(50 * time.Millisecond) // Petite pause entre les requêtes\n }\n}\n\nLes 5 premières requêtes passeront instantanément (le buffer est plein), puis une toutes les 200 ms. Ce modèle est parfait pour les API où le client peut soudainement envoyer beaucoup de requêtes (par exemple après un redémarrage).
\n\nThrottling avec WaitGroup et Worker Pool
\n\nPassons maintenant au throttling. Supposons que nous ayons une file de tâches (par exemple, l'envoi d'une newsletter par email). Nous ne voulons pas bloquer le client, mais nous ne voulons pas non plus surcharger le serveur.