Rate Limiting

Le rate limiting est une technique fondamentale de contrôle du trafic qui limite le nombre de requêtes qu'un client peut effectuer vers une API ou un service web au cours d'une fenêtre de temps spécifique, protégeant l'infrastructure contre la surcharge, les attaques par déni de service (DoS/DDoS), l'abus automatisé, le scraping malveillant et l'utilisation inappropriée de ressources de calcul limitées. Dans un scénario où les API modernes traitent des millions de requêtes par seconde provenant d'applications mobiles, d'intégrations de partenaires, de bots légitimes et potentiellement d'attaquants malveillants, l'absence d'un rate limiting adéquat peut entraîner des coûts opérationnels explosifs, une dégradation sévère des performances pour les utilisateurs légitimes, des vulnérabilités aux attaques par credential stuffing et par force brute, et même l'indisponibilité totale du service. La mise en œuvre efficace du rate limiting nécessite une compréhension approfondie des différents algorithmes (token bucket, leaky bucket, fixed window, sliding window), des considérations sur l'architecture distribuée où plusieurs serveurs doivent partager des compteurs de débit, des stratégies d'identification des clients (IP, API key, JWT, session), des politiques différenciées par niveau d'utilisateur (free, premium, enterprise), et des mécanismes de communication claire via des HTTP headers standardisés qui informent les clients sur les limites, la consommation actuelle et le temps de réinitialisation. Cet article explore les algorithmes, les patterns d'implémentation, les outils et les best practices pour construire des systèmes robustes de rate limiting.

Algorithmes de Rate Limiting

1. Token Bucket (Le Plus Courant)

      # Concept : Bucket avec capacité maximale de tokens
      # Les tokens sont ajoutés à débit constant
      # Chaque request consomme 1 token
      # Si le bucket est vide, le request est rejeté
      Capacité : 100 tokens
      Débit de recharge : 10 tokens/seconde
      Avantages :
      - Permet des bursts contrôlés (bucket plein)
      - Simple à implémenter
      - Flexible pour différents patterns de trafic
      Inconvénients :
      - Peut permettre des bursts qui surchargent le système
      - Nécessite le tracking de la dernière recharge
      Implémentation :
      class TokenBucket {
      constructor(capacity, refillRate) {
      this.capacity = capacity;
      this.tokens = capacity;
      this.refillRate = refillRate;
      this.lastRefill = Date.now();
      }
      consume(count = 1) {
      this.refill();
      if (this.tokens >= count) {
      this.tokens -= count;
      return true;
      }
      return false;
      }
      refill() {
      const now = Date.now();
      const elapsed = (now - this.lastRefill) / 1000;
      const tokensToAdd = elapsed * this.refillRate;
      this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
      this.lastRefill = now;
      }
      }
      

2. Leaky Bucket

      # Concept : Queue avec écoulement constant
      # Les requests entrent dans le bucket (queue)
      # Traités à débit constant (leak)
      # Si la queue est pleine, les requests sont rejetés
      Avantages :
      - Lisse les bursts (traffic shaping)
      - Output rate constant et prévisible
      - Protège le backend des spikes
      Inconvénients :
      - Peut ajouter de la latence (queueing)
      - Complexité d'implémentation
      Usage :
      - Traffic shaping
      - Network gateways
      - Quand un output rate constant est critique
      

3. Fixed Window Counter

      # Concept : Compteur par fenêtre de temps fixe
      # Exemple : 100 requests par minute
      # Réinitialisation au début de chaque minute (XX:00, XX:01, XX:02...)
      Avantages :
      - Extrêmement simple
      - Memory efficient
      - Facile à comprendre
      Inconvénients :
      - Edge case : 200 requests en 1 seconde
      (100 à la fin de la minute 1, 100 au début de la minute 2)
      - Permet des bursts à la limite des fenêtres
      Implémentation Redis :
      INCR user:123:2024-01-15:14:30
      EXPIRE user:123:2024-01-15:14:30 60
      GET user:123:2024-01-15:14:30  # Si > 100, reject
      

4. Sliding Window Log

      # Concept : Log des timestamps des requests
      # Supprime les requests en dehors de la fenêtre
      # Compte les requests à l'intérieur de la fenêtre glissante
      Avantages :
      - Précision parfaite
      - Pas d'edge cases de fixed window
      - Distribue le débit uniformément
      Inconvénients :
      - Memory intensive (stocke tous les timestamps)
      - Les performances se dégradent avec un high traffic
      Implémentation Redis (Sorted Set) :
      ZADD user:123 <timestamp> <request-id>
      ZREMRANGEBYSCORE user:123 0 <timestamp-60s>  # Supprime les anciens
      ZCARD user:123  # Count requests
      Si > 100, reject
      

5. Sliding Window Counter (Hybride)

      # Concept : Combine fixed window avec sliding
      # Utilise les compteurs des fenêtres précédentes avec pondération
      # Estime le débit dans une sliding window
      Exemple : Limite 100/minute
      Fenêtre actuelle (14:30) : 70 requests
      Fenêtre précédente (14:29) : 90 requests
      Écoulé dans la fenêtre actuelle : 40s (66.7%)
      Estimation : 90 * (1 - 0.667) + 70 = 30 + 70 = 100
      Avantages :
      - Précision proche de sliding log
      - Memory efficient (seulement 2 compteurs)
      - Lisse les bursts
      Inconvénients :
      - Estimation (pas exact)
      - Plus complexe que fixed window
      

Distributed Rate Limiting

      # Problème : Plusieurs serveurs doivent partager le state
      # Solutions :
      1. Redis Centralisé (Le Plus Courant)
      const redis = require('redis');
      const client = redis.createClient();
      async function checkRateLimit(userId) {
      const key = \`rate:\$:\${getCurrentWindow()}\`;
      const count = await client.incr(key);
      if (count === 1) {
      await client.expire(key, 60); // 60 seconds
      }
      return count <= 100; // Limit: 100/min
      }
      2. Redis Lua Script (Atomic)
      const luaScript = \`
      local key = KEYS[1]
      local limit = tonumber(ARGV[1])
      local current = redis.call('incr', key)
      if current == 1 then
      redis.call('expire', key, ARGV[2])
      end
      if current > limit then
      return 0
      end
      return 1
      \`;
      3. Sticky Sessions + Local Counters
      - Router l'utilisateur toujours vers le même server
      - Counter local sur le serveur
      - Problème : Ne fonctionne pas bien avec l'auto-scaling
      4. Gossip Protocol
      - Les serveurs partagent le state via gossip
      - Eventual consistency
      - Plus complexe, utilisé en high scale
      

HTTP Headers Standards

      # Standards (RFCs)
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 45
      X-RateLimit-Reset: 1640000000  # Unix timestamp
      # Quand la limite est dépassée
      HTTP/1.1 429 Too Many Requests
      Retry-After: 60  # Secondes avant le retry
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 0
      X-RateLimit-Reset: 1640000060
      {
      "error": "Rate limit exceeded",
      "retryAfter": 60,
      "limit": 100
      }
      # Style GitHub (plus informatif)
      X-RateLimit-Limit: 5000
      X-RateLimit-Remaining: 4999
      X-RateLimit-Reset: 1372700873
      X-RateLimit-Used: 1
      X-RateLimit-Resource: core
      

Implémentation avec Express.js

      const rateLimit = require('express-rate-limit');
      const RedisStore = require('rate-limit-redis');
      const redis = require('redis');
      const client = redis.createClient();
      // Basic rate limiter
      const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      standardHeaders: true, // Return rate limit info in headers
      legacyHeaders: false,
      message: 'Too many requests, please try again later.'
      });
      app.use('/api/', limiter);
      // Redis-based distributed rate limiting
      const distributedLimiter = rateLimit({
      store: new RedisStore({
      client: client,
      prefix: 'rate-limit:',
      }),
      windowMs: 60 * 1000,
      max: 10,
      standardHeaders: true,
      });
      // Different limits per route
      const authLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 5, // Stricter for auth endpoints
      skipSuccessfulRequests: true, // Don't count successful logins
      });
      app.post('/api/login', authLimiter, loginHandler);
      // Custom key function (rate limit by user ID instead of IP)
      const userLimiter = rateLimit({
      windowMs: 60 * 1000,
      max: 100,
      keyGenerator: (req) => req.user.id, // Requires auth middleware
      });
      

Tiered Rate Limiting

      // Different limits based on user tier
      function getRateLimit(user) {
      const tiers = {
      free: { windowMs: 3600000, max: 100 },      // 100/hour
      basic: { windowMs: 3600000, max: 1000 },    // 1000/hour
      premium: { windowMs: 3600000, max: 10000 }, // 10k/hour
      enterprise: { windowMs: 3600000, max: 100000 } // 100k/hour
      };
      return tiers[user.tier] || tiers.free;
      }
      app.use(async (req, res, next) => {
      const user = await getUserFromToken(req);
      const limits = getRateLimit(user);
      const limiter = rateLimit({
      ...limits,
      keyGenerator: () => user.id,
      });
      limiter(req, res, next);
      });
      

Outils et Services

  • Redis : Compteurs distribués, expire automatique
  • Kong : API Gateway avec plugin de rate limiting
  • Nginx rate limiting : limit_req_zone, limit_conn_zone
  • Cloudflare : Rate limiting au niveau du CDN
  • AWS API Gateway : Throttling intégré
  • express-rate-limit : Middleware Express
  • Tyk : API gateway open-source

Best Practices

  • Choisissez l'algorithme adéquat : Token bucket pour les API générales, sliding window pour la précision
  • Limites différenciées : Endpoints d'auth plus restrictifs, read-only plus permissifs
  • Communication claire : Headers informatifs, messages d'erreur utiles
  • Whitelist : IPs de partenaires de confiance, health checks
  • Monitoring : Alerter lorsque les utilisateurs atteignent fréquemment les limites
  • Graceful degradation : Retourner des cached data si possible
  • Distributed state : Utiliser Redis pour les déploiements multi-serveurs
  • Cost-based limiting : Les opérations coûteuses consomment plus de tokens

Recommandations

Pour les API modernes, implémentez un token bucket avec Redis pour le distributed rate limiting. Utilisez un sliding window counter lorsque vous avez besoin de précision sans overhead de mémoire. Configurez des limites différenciées : 5 req/min pour le login, 100 req/min pour la lecture, 10 req/min pour les opérations d'écriture. Retournez toujours des headers informatifs et implémentez une retry logic avec exponential backoff côté clients.