Skip to Content
SecurityRate Limiting & Throttling

Rate Limiting & Throttling

ProtĂ©ger une API contre les abus, les DDoS et la surconsommation de ressources en limitant le nombre de requĂȘtes par client sur une fenĂȘtre de temps donnĂ©e.

Pourquoi limiter ?

RisqueImpact sans limite
Surcharge du backendTemps de réponse qui explose, crash en cascade
Abus d’API (scraping, crawler)CoĂ»ts infrastructure, bande passante gaspillĂ©e
DDoSIndisponibilité du service
Iniquité entre clientsUn client monopolise les ressources
Surcharge de la base de donnéesConnexions MySQL/PostgreSQL épuisées

Le rate limiting n’est pas de la sĂ©curitĂ© pure — c’est de la rĂ©silience opĂ©rationnelle. MĂȘme une API bien conçue et bien authentifiĂ©e peut tuer le service si un seul client envoie 10 000 requĂȘtes par seconde.

StratĂ©gies d’algorithme

Fixed Window

Coupe le temps en fenĂȘtres fixes (ex. 1 minute). Compteur reset Ă  chaque changement de fenĂȘtre.

fenĂȘtre 0: |● ● ● ● ● ● ● ● ● ● | → 10 req, OK fenĂȘtre 1: |● ● ● ● ● ● ● ● ● ● | → 10 req, OK
  • Avantage : simple Ă  implĂ©menter (un compteur + timestamp de dĂ©but de fenĂȘtre).
  • InconvĂ©nient : pic aux limites de fenĂȘtre. Un client peut envoyer 10 requĂȘtes en 0,5 s juste avant la coupure, puis 10 autres juste aprĂšs.

Sliding Window Log

Conserve l’historique de chaque requĂȘte (timestamp). Compte les requĂȘtes dans la fenĂȘtre glissante.

  • Avantage : lissage parfait entre les fenĂȘtres.
  • InconvĂ©nient : coĂ»t mĂ©moire (il faut stocker chaque timestamp).

Sliding Window Counter (approximatif)

Combine deux fenĂȘtres fixes (ancienne + nouvelle) pondĂ©rĂ©es par la proportion du temps Ă©coulĂ© dans la fenĂȘtre courante. Environnement Redis typique pour le gateway.

Token Bucket

Un « seau » se remplit Ă  taux constant (replenishRate). Chaque requĂȘte consomme un jeton. Si le seau est vide, la requĂȘte est rejetĂ©e.

bucket: 20 jetons max, 1 jeton/sec → 20 req immĂ©diates puis 1 req/sec
  • Avantage : lisse les bursts (le seau se remplit lentement).
  • InconvĂ©nient : nĂ©cessite un systĂšme distribuĂ© pour fonctionner Ă  plusieurs instances.

Leaky Bucket

File d’attente avec dĂ©bit de sortie constant. Les requĂȘtes en excĂšs sont rejetĂ©es.

  • Avantage : dĂ©bit de sortie toujours constant (utile pour protĂ©ger des backends fragiles).
  • InconvĂ©nient : les requĂȘtes en excĂšs sont perdues (pas de mise en file d’attente).

ImplĂ©mentation Spring Boot — Resilience4j

Dépendance

<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> </dependency>

Annotation @RateLimiter sur un service

@RateLimiter(name = "apiRateLimiter", fallbackMethod = "fallback") public Mono<ResponseEntity<ProduitDTO>> obtenirProduit(UUID id) { ProduitDTO produit = service.obtenir(id); return Mono.just(ResponseEntity.ok(produit)); } // Méthode appelée quand le rate limit est atteint public Mono<ResponseEntity<ProduitDTO>> fallback( UUID id, RateLimiterNotPermittedException ex) { return Mono.just(ResponseEntity .status(HttpStatus.TOO_MANY_REQUESTS) .body(null)); }

Configuration YAML

resilience4j: ratelimiter: instances: apiRateLimiter: limitForPeriod: 100 # requĂȘtes max par fenĂȘtre limitRefreshPeriod: 60s # durĂ©e de la fenĂȘtre timeoutDuration: 0s # attente avant rejet (0 = rejet immĂ©diat) registerHealthIndicator: true

En-tĂȘtes ajoutĂ©s automatiquement (Spring Boot 3.2+)

Depuis Spring Boot 3.2, l’annotation @RateLimiter ajoute automatiquement les en-tĂȘtes X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset Ă  la rĂ©ponse.

Note : les en-tĂȘtes sont ajoutĂ©s automatiquement depuis Spring Boot 3.2 avec Resilience4j 2.x. Pour les versions antĂ©rieures, il faut configurer un filtre personnalisĂ© pour les ajouter.

Implémentation Spring Cloud Gateway

Le gateway est le lieu idĂ©al pour le rate limiting : il voit toutes les requĂȘtes, avant qu’elles n’atteignent les services.

Architecture Redis-backed

Client → Gateway → [RequestRateLimiter] → Service ↓ Redis (compteur partagĂ©)

Redis permet un compteur partagĂ© entre plusieurs instances du gateway — indispensable en production avec load balancing.

Configuration YAML

spring: cloud: gateway: routes: - id: service-public uri: lb://service-public predicates: - Path=/api/public/** filters: - name: RequestRateLimiter args: key-resolver: "#{@ipKeyResolver}" redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1

Clés de résolution

// KeyResolver par IP (prĂȘt Ă  l'emploi) @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just( exchange.getRequest().getRemoteAddress() .getAddress().getHostAddress() ); } // KeyResolver par identifiant utilisateur (aprĂšs authentification JWT) @Bean public KeyResolver userKeyResolver() { return exchange -> Mono.just( exchange.getAttribute("authenticatedPrincipal") .orElse("anonymous") ); } // KeyResolver combinĂ© : utilisateur si authentifiĂ©, sinon IP @Bean public KeyResolver compositeKeyResolver() { return exchange -> { String principal = exchange.getAttribute("authenticatedPrincipal"); if (principal != null) { return Mono.just(principal); } return Mono.just( exchange.getRequest().getRemoteAddress() .getAddress().getHostAddress() ); }; }

PiĂšge : RequestRateLimiter utilise Sliding Window Counter via Redis. replenishRate dĂ©finit le dĂ©bit continu, burstCapacity le nombre maximal de requĂȘtes en rafale. requestedTokens: 1 = une requĂȘte = un jeton.

Filtrage conditionnel

Appliquer le rate limiting uniquement à certains chemins ou méthodes :

spring: cloud: gateway: routes: - id: service-public uri: lb://service-public predicates: - Path=/api/public/** - Method=GET,POST filters: - name: RequestRateLimiter args: key-resolver: "#{@userKeyResolver}" redis-rate-limiter.replenishRate: 5 redis-rate-limiter.burstCapacity: 10 redis-rate-limiter.requestedTokens: 1

Implémentation Micronaut

Dépendance

<dependency> <groupId>io.micronaut.micrometer</groupId> <artifactId>micronaut-micrometer-core</artifactId> </dependency> <dependency> <groupId>io.micronaut.micrometer</groupId> <artifactId>micronaut-micrometer-registry-prometheus</artifactId> </dependency>

Annotation @RateLimiter (Micronaut 4+)

@Singleton public class ProduitService { @RateLimiter(name = "produitRateLimiter") public ProduitDTO obtenirProduit(UUID id) { return repo.obtenir(id) .orElseThrow(() -> new NotFoundException("Produit introuvable")); } @ExceptionHandler(RateLimiterExhaustedException.class) @Status(HttpStatus.TOO_MANY_REQUESTS) public HttpResponse<String> onRateLimit() { return HttpResponse.status(HttpStatus.TOO_MANY_REQUESTS) .body("Trop de requĂȘtes — rĂ©essayez plus tard"); } }

Configuration YAML

micronaut: time: rate-limiter: enabled: true default: maximum-permits: 100 permit-refresh-period: 60s permit-refresh-period-time-unit: SECONDS

En-tĂȘtes de rĂ©ponse standard

RFC 7231 et pratiques courantes. Les clients (react-app, mobile) les lisent pour afficher un spinner ou un message.

En-tĂȘteSignificationExemple
X-RateLimit-LimitQuota maximum sur la fenĂȘtreX-RateLimit-Limit: 100
X-RateLimit-RemainingRequĂȘtes restantes dans la fenĂȘtreX-RateLimit-Remaining: 42
X-RateLimit-ResetEpoch timestamp (Unix) du resetX-RateLimit-Reset: 1720000000
Retry-AfterDĂ©lai en secondes avant de renvoyer une requĂȘte (uniquement sur 429)Retry-After: 30

Exemple de réponse 429

HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1720000030 Retry-After: 30 Content-Type: application/json {"error": "Too many requests", "retryAfterSeconds": 30}

RĂšgle : toujours renvoyer un code 429 Too Many Requests (et jamais un 403 ou 200 cachĂ©) quand le quota est dĂ©passĂ©. Les clients doivent ĂȘtre capables de le dĂ©tecter.

PiĂšges

1. Spoofing d’IP avec X-Forwarded-For

Si le gateway se trouve derriùre un proxy (NGINX, AWS ALB), getRemoteAddress() retourne l’adresse du proxy, pas du client.

// ❌ MAUVAIS — facile à spoof String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); // ✅ BON — lire le header de confiance (uniquement si le proxy est de confiance) String forwarded = exchange.getRequest().getHeaders() .getFirst("X-Forwarded-For"); String ip = (forwarded != null) ? forwarded.split(",")[0].trim() : exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();

PiĂšge : un attaquant peut ajouter l’en-tĂȘte X-Forwarded-For dans sa requĂȘte pour tromper le rate limiter. Seules les adresses IP de confiance (le proxy) doivent ĂȘtre autorisĂ©es Ă  l’envoyer.

2. Rate limiting bypassé par un header tronqué

Certains frameworks ignorent les en-tĂȘtes dont la longueur est trop grande. Pour le X-RateLimit-Remaining, un client qui affiche un compteur ne le voit jamais diminuer — mais le serveur continue Ă  compter. Le client doit refaire la requĂȘte quand il atteint zĂ©ro.

3. Test du rate limit inadapté

Tester avec un seul thread cache la consommation réelle. Utiliser un outil de load test (k6, JMeter, Artillery) pour simuler plusieurs clients simultanément.

# Exemple k6 : 100 requĂȘtes/s pendant 30 secondes cat <<'EOF' > rate-limit-test.js import http from 'k6/http'; export const options = { vus: 100, duration: '30s', }; export default function () { http.get('http://localhost:8080/api/public/produits'); } EOF k6 run rate-limit-test.js

4. Failover Redis → pas de rate limiting

Si Redis devient injoignable, RequestRateLimiter ne bloque pas — il laisse passer toutes les requĂȘtes. Toujours avoir un mĂ©canisme de repli : circuit breaker vers un fallback in-memory ou bloquer les requĂȘtes.

spring: cloud: gateway: routes: - id: service-public uri: lb://service-public predicates: - Path=/api/public/** filters: - name: RequestRateLimiter args: key-resolver: "#{@userKeyResolver}" redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 # Circuit breaker en fallback si Redis est mort - name: CircuitBreaker args: name: rateLimiterCircuit fallbackUri: forward:/fallback/rate-limit

5. FenĂȘtre de temps mal calibrĂ©e

  • FenĂȘtre trop courte (1 s) : le client lĂ©gitime est pĂ©nalisĂ© pour des tests rapides.
  • FenĂȘtre trop longue (1 heure) : un attaquant a une fenĂȘtre de tir Ă©norme.
  • Recommandation : 60 s pour la plupart des APIs, ajuster selon le type d’endpoint (lecture vs Ă©criture).

Checklist de déploiement

  • StratĂ©gie d’algorithme choisie (token bucket recommandĂ© pour la plupart des cas)
  • SystĂšme distribuĂ© de comptage (Redis, Redis Cluster, ou serveur de consensus)
  • ClĂ© de rĂ©solution adaptĂ©e (utilisateur authentifiĂ© si possible, IP sinon)
  • En-tĂȘtes X-RateLimit-* ajoutĂ©s Ă  chaque rĂ©ponse
  • Code 429 renvoyĂ© correctement avec Retry-After
  • Rejet non silencieux : le client reçoit bien un message d’erreur
  • Test de charge rĂ©alisĂ© (k6, JMeter, Artillery)
  • Fallback dĂ©fini si le systĂšme de comptage devient injoignable
  • Monitoring des indicateurs (quota utilisĂ©, taux de 429, pics)