Rate limiting d'API : protéger un SaaS sans casser l'expérience utilisateur

par Benjamin

Une API peut tomber sans que l'infrastructure soit en panne. Il suffit parfois qu'un client mal configuré, un script de synchronisation ou un bot répète la même requête assez vite pour consommer la capacité disponible.

Le rate limiting, ou limitation de débit, sert à contrôler le nombre de requêtes acceptées pendant une période donnée. Bien conçu, il protège les ressources partagées et rend la capacité prévisible. Mal conçu, il bloque les bons utilisateurs, masque un problème de performance et transforme chaque 429 Too Many Requests en ticket support.

Le sujet n'est donc pas de choisir un nombre de requêtes par minute. Il faut décider ce que l'on protège, qui l'on protège, comment on mesure la consommation et quoi doit faire le client lorsqu'il atteint la limite.

Le rate limiting protège une capacité, pas seulement un serveur

Avant de définir une limite, identifiez la ressource qui sature réellement :

  • une base de données, lorsqu'une requête coûteuse monopolise les connexions ;
  • une API tierce, qui impose son propre quota ;
  • un worker, dont la file d'attente prend du retard ;
  • un fournisseur d'IA facturé au volume de tokens ;
  • un endpoint sensible, comme la connexion ou la réinitialisation de mot de passe ;
  • une capacité globale partagée entre plusieurs clients.

La même limite ne convient pas à tous les endpoints. Une lecture en cache, une recherche complexe et une opération qui déclenche un paiement n'ont ni le même coût ni le même risque.

Commencez par cartographier le parcours et la ressource protégée :

Endpoint ou action Ressource à protéger Risque principal Réponse attendue
Recherche Base de données surcharge et latence limite par utilisateur ou clé API
Export Worker et stockage traitements longs quota et mise en file
Création de commande Métier et fournisseur de paiement doublons et coût idempotence, limite prudente
Connexion Système d'authentification force brute limite par compte et par origine
Appel d'une API tierce Quota fournisseur erreurs en cascade limite globale et file d'attente

Cette étape évite une erreur fréquente : poser un middleware de limitation uniforme sur toute l'application, alors que le problème se trouve dans un seul chemin coûteux.

Choisir la bonne clé de limitation

Une limite n'a de sens que si l'application sait à qui ou à quoi elle s'applique. Les clés courantes sont :

  • adresse IP, utile avant authentification et contre certains abus simples ;
  • utilisateur authentifié, plus juste pour un produit connecté ;
  • organisation ou tenant, nécessaire lorsque plusieurs utilisateurs partagent une capacité ;
  • clé API ou application cliente, adaptée aux intégrations externes ;
  • route et méthode HTTP, pour distinguer les coûts ;
  • ressource métier, lorsque la protection concerne une opération précise.

L'adresse IP ne doit pas devenir la seule identité d'un SaaS. Plusieurs utilisateurs légitimes peuvent partager une adresse, par exemple derrière un réseau d'entreprise ou un opérateur mobile. À l'inverse, un attaquant peut répartir ses requêtes sur plusieurs adresses.

En pratique, je combine souvent plusieurs niveaux : une protection réseau avant authentification, une limite par identité après authentification, puis un budget par tenant pour éviter qu'un client consomme toute la capacité commune.

Attention à l'infrastructure intermédiaire. Si l'application est derrière un proxy, un load balancer ou un CDN, l'adresse visible par le serveur peut être celle du proxy. La configuration de confiance des en-têtes de transfert doit être explicite. Faire confiance à n'importe quel X-Forwarded-For permettrait à un client de changer lui-même sa clé de limitation.

Token bucket, fenêtre glissante ou file d'attente ?

L'algorithme détermine le comportement de l'application sous charge. Il faut le choisir en fonction de la capacité à protéger, pas parce qu'il est populaire.

Fenêtre fixe

La fenêtre fixe compte les requêtes sur des périodes distinctes, par exemple de 12:00:00 à 12:00:59. Elle est simple à expliquer et à implémenter.

Son défaut est l'effet de bordure : un client peut envoyer son quota à la fin d'une fenêtre puis un nouveau quota au début de la suivante. La capacité réellement consommée sur un court intervalle devient alors beaucoup plus élevée que prévu.

Fenêtre glissante

La fenêtre glissante regarde les requêtes sur une période qui se déplace avec le temps. Elle réduit l'effet de bordure, au prix d'un état plus important et d'un calcul plus coûteux selon l'implémentation.

Elle convient lorsque l'on veut une limite assez régulière sur une période courte, mais elle ne règle pas à elle seule le problème des opérations dont le coût varie fortement.

Token bucket

Le token bucket, ou seau de jetons, associe un débit de reconstitution à une capacité de burst. Chaque requête consomme un jeton. Le seau peut absorber une courte pointe lorsqu'il est plein, puis impose le débit moyen configuré.

C'est un bon choix lorsque l'on accepte des bursts contrôlés, mais que l'on veut empêcher une montée continue. AWS documente ce modèle pour plusieurs de ses API et distingue bien le débit moyen de la capacité de burst. [^1]

File d'attente et limitation de concurrence

Toutes les requêtes ne doivent pas être rejetées. Pour un export, un recalcul ou un appel vers un fournisseur externe, il peut être préférable d'accepter la demande, de la placer dans une file et de limiter le nombre de traitements simultanés.

Cette approche ne remplace pas le rate limiting. Elle protège une capacité différente : le nombre de travaux en cours et non le nombre de demandes entrantes. Une file sans limite finit par déplacer la saturation vers la mémoire, le stockage ou le délai d'attente.

Le pattern de throttling d'Azure rappelle qu'un token bucket convient aux bursts, qu'un leaky bucket régule un débit plus constant, et qu'une limite de concurrence est utile lorsque le point de saturation est le nombre d'opérations simultanées. [^2]

Définir une politique par coût et par client

Une limite globale peut empêcher un client de monopoliser l'API, mais elle peut aussi pénaliser tout le monde dès qu'un seul tenant devient actif. Une limite par client améliore l'équité, mais elle ne protège pas suffisamment la capacité commune si le nombre de clients augmente.

Il faut donc séparer au moins trois niveaux :

  1. Protection globale, pour préserver la capacité totale du service.
  2. Budget par tenant, pour empêcher un client de prendre toute la capacité.
  3. Limite par opération, pour encadrer les routes coûteuses ou sensibles.

Le budget doit refléter le modèle produit. Un plan payant peut avoir une capacité supérieure, mais cela ne signifie pas que toutes ses opérations doivent être illimitées. Une règle utile est d'associer chaque limite à une ressource et à une promesse compréhensible : nombre d'exports simultanés, volume de synchronisation, appels d'API ou fréquence d'une action métier.

Évitez de promettre un chiffre que l'architecture ne peut pas tenir. Une limite affichée dans la documentation devient une contrainte attendue par les clients. Si elle évolue, la politique et le comportement de l'API doivent rester compatibles avec le contrat annoncé.

Que répondre avec un 429 ?

Le statut HTTP 429 Too Many Requests indique qu'un client a dépassé une limite de requêtes. RFC 6585 précise que la réponse peut inclure Retry-After, afin d'indiquer combien de temps le client doit attendre avant de réessayer. [^3]

Une réponse exploitable devrait contenir :

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 12
Cache-Control: no-store

{
  "type": "https://api.example.com/problems/rate-limit-exceeded",
  "title": "Trop de requêtes",
  "status": 429,
  "detail": "La limite de recherche a été atteinte.",
  "retry_after": 12
}

Le format application/problem+json permet de fournir une erreur structurée et prévisible. RFC 9457 définit ce format pour transporter des détails lisibles par les machines et les humains, sans inventer un format différent pour chaque endpoint. [^4]

Retry-After peut exprimer un délai en secondes ou une date HTTP. Si le serveur connaît le moment de réouverture, il doit donner une indication précise. Dans tous les cas, le client ne doit pas être obligé de deviner une durée arbitraire.

Ne renvoyez pas systématiquement 429 pour une saturation interne qui n'est pas imputable au client. Si la ressource est temporairement indisponible pour une raison générale, 503 Service Unavailable avec un Retry-After peut être plus approprié. La distinction aide le client à adapter son comportement et l'équipe à comprendre l'origine de la dégradation. [^5]

Le client doit ralentir sans créer une nouvelle pointe

Un client qui reçoit 429 ne doit pas réessayer immédiatement en boucle. Le comportement minimal est :

  1. lire Retry-After lorsqu'il est fourni ;
  2. attendre au moins le délai indiqué ;
  3. appliquer un backoff progressif pour les erreurs répétées ;
  4. ajouter un peu d'aléa, ou jitter, afin que tous les clients ne repartent pas au même instant ;
  5. respecter un nombre maximal de tentatives ;
  6. abandonner ou mettre en file la demande lorsque son résultat n'est plus utile.

Le retry ne doit pas être activé indistinctement. Une lecture peut souvent être rejouée. Une création ou un paiement exige une clé d'idempotence et une stratégie claire, sinon le client peut transformer une limitation en double opération métier.

Pour un SDK, cette politique doit être centralisée. Chaque appelant ne doit pas réinventer ses propres délais et ses propres exceptions. Pour une interface utilisateur, le message doit expliquer l'action possible : attendre, relancer manuellement, ou réduire la période demandée. Montrer seulement "Erreur 429" transfère la complexité technique à l'utilisateur.

Dans une architecture distribuée, le compteur doit être partagé

Un rate limiter stocké uniquement en mémoire dans chaque instance fonctionne tant qu'il n'y a qu'un serveur. Dès que plusieurs instances reçoivent du trafic, chacune voit une partie des requêtes et la limite globale peut être dépassée.

Vous avez alors plusieurs options :

  • un compteur partagé dans Redis ou un service équivalent ;
  • une limitation au niveau d'un gateway ou d'un reverse proxy ;
  • un quota calculé par un service dédié ;
  • une file centrale pour les traitements asynchrones ;
  • une combinaison d'une limite locale rapide et d'un budget partagé.

Le choix dépend de la précision requise et du coût d'une coordination distribuée. Pour une protection anti-abus, une approximation peut suffire. Pour un quota contractuel ou un appel vers un fournisseur facturé, la décision doit être beaucoup plus exacte.

Le stockage partagé devient lui-même une dépendance critique. Si le compteur est indisponible, faut-il laisser passer, refuser ou appliquer une limite locale de secours ? La réponse dépend de la ressource protégée. Pour un endpoint de connexion, il est généralement préférable d'échouer de manière restrictive. Pour une lecture non sensible, une dégradation temporaire peut être acceptable.

Documentez ce comportement avant l'incident. Un fallback implicite est rarement le bon fallback.

Mesurer si la limitation protège réellement le produit

Un rate limiter ne se juge pas seulement au nombre de 429 retournés. Suivez au minimum :

  • le nombre de requêtes limitées, par route, tenant et client ;
  • le taux de 429 parmi les requêtes légitimes ;
  • les tentatives répétées après un 429 ;
  • la latence et le taux d'erreur de la ressource protégée ;
  • la longueur des files et l'âge du message le plus ancien ;
  • la consommation par tenant et par plan ;
  • les demandes abandonnées après limitation ;
  • les erreurs du composant qui stocke les compteurs.

Un pic de 429 peut signifier un abus, une limite trop basse, une nouvelle version qui déclenche trop de requêtes ou un client qui ignore le contrat. Sans distinguer ces cas, l'équipe ne sait pas si elle doit bloquer, corriger, documenter ou augmenter la capacité.

Reliez les métriques techniques aux parcours produit. Une baisse du taux d'activation ou une hausse des exports abandonnés peut révéler une limite invisible dans l'interface. Cela complète le travail d'observabilité d'un SaaS : les signaux doivent décrire l'expérience et non seulement l'état des composants.

Les erreurs que je corrige le plus souvent

Une seule limite pour toute l'API

Elle est facile à ajouter, mais elle mélange les coûts et produit des décisions incompréhensibles. Une route de lecture bon marché ne devrait pas partager exactement le même budget qu'une opération qui appelle trois fournisseurs.

Une limite basée uniquement sur l'adresse IP

Elle pénalise les réseaux partagés et se contourne facilement avec plusieurs origines. Elle peut rester une première barrière, mais pas l'identité principale d'un SaaS authentifié.

Un compteur en mémoire dans chaque instance

Il donne une illusion de protection dès que l'application est répliquée. Le trafic réel et le trafic compté ne sont plus les mêmes.

Un retry automatique sans limite

Il transforme un refus temporaire en tempête de requêtes. Le retry doit tenir compte de Retry-After, du coût de l'opération et d'un nombre maximal de tentatives.

Une limite qui bloque les opérations métier critiques

Si un client ne peut pas terminer une action importante, le produit doit l'indiquer et proposer une stratégie. Certaines actions doivent être mises en file plutôt que rejetées, d'autres doivent être protégées par idempotence avant tout retry.

Des limites non testées sous charge

Une politique qui semble correcte sur un environnement calme peut échouer lorsque plusieurs instances écrivent le compteur en même temps. Testez les bursts, les tenants concurrents, les expirations, les pannes du stockage partagé et les retries synchronisés.

Une démarche pragmatique pour l'implémenter

Je procède par étapes :

  1. Choisir un seul parcours coûteux ou sensible. Mesurer son volume, son coût et sa capacité réelle.
  2. Définir la clé et la portée. IP avant connexion, identité après connexion, tenant pour la capacité partagée.
  3. Choisir l'algorithme. Token bucket pour des bursts contrôlés, fenêtre glissante pour une fréquence régulière, file et limite de concurrence pour les traitements longs.
  4. Formaliser le contrat. Statut 429, Retry-After, format d'erreur, limites documentées et comportement attendu du client.
  5. Tester les cas de concurrence. Plusieurs instances, plusieurs requêtes simultanées, expiration et panne du compteur.
  6. Instrumenter avant d'élargir. Mesurer les refus, la latence, les parcours abandonnés et la consommation par tenant.
  7. Étendre seulement si la protection est comprise. Une limite de plus ajoute une règle d'exploitation et une surface de support.

Cette méthode permet d'éviter deux extrêmes : ne rien limiter jusqu'au premier incident, ou installer un système sophistiqué que personne ne sait ajuster.

FAQ : rate limiting et API

Quelle différence entre rate limiting et throttling ?

Les deux termes sont souvent utilisés pour parler de contrôle de débit. Dans une conception plus précise, le rate limiting refuse au-delà d'un budget, tandis que le throttling peut ralentir ou réguler le flux. Le choix concret dépend de la ressource et du comportement attendu du client.

Quelle limite choisir pour une API SaaS ?

Il n'existe pas de valeur universelle. Mesurez le coût de l'opération, la capacité disponible et le volume attendu par tenant. Définissez ensuite un budget global et des limites par opération, puis testez-les avec des bursts et des clients concurrents.

Faut-il toujours renvoyer Retry-After avec un 429 ?

C'est fortement recommandé lorsque le serveur peut estimer le délai avant une nouvelle tentative. Le client pourra alors ralentir correctement au lieu de choisir une durée arbitraire. La réponse doit aussi préciser l'erreur dans un format stable.

Une limite par adresse IP suffit-elle ?

Non pour un SaaS authentifié. Une adresse IP peut être partagée par plusieurs utilisateurs et ne représente pas nécessairement le client qui consomme la capacité. Combinez-la avec l'identité, la clé API ou le tenant selon le contexte.

Que faire si le rate limiter est indisponible ?

Décidez d'un comportement de secours par type de ressource. Une opération sensible peut échouer fermement, tandis qu'une lecture non critique peut appliquer une limite locale temporaire. Dans les deux cas, l'indisponibilité du compteur doit être mesurée et alertée.

Ce que je recommande, en tant que CTO hands-on

Le rate limiting n'est pas une barrière que l'on pose devant une API puis que l'on oublie. C'est un contrat entre votre capacité technique, vos clients et les logiciels qui consomment votre service.

Commencez par la ressource qui peut réellement mettre le produit en difficulté. Limitez-la avec une clé équitable, répondez avec un 429 exploitable, rendez le retry prévisible et mesurez les effets sur les parcours utilisateurs. Ensuite seulement, généralisez la politique.

Une bonne limite ne cherche pas à bloquer le plus de requêtes possible. Elle rend la capacité partageable, les erreurs récupérables et les arbitrages visibles. C'est ce qui permet de protéger un SaaS sans transformer sa fiabilité en mauvaise expérience.

Sources

Benjamin Raimond

Benjamin Raimond

CTO hands-on : j'architecture, je code, je structure l'équipe technique. Je transforme des objectifs business en solutions robustes et durables pour les startups et PME.

En savoir plus →