HTTP 429 — « Too Many Requests »

Le code 429 (défini par la RFC 6585 §4, sémantique reprise dans l'écosystème HTTP/1.1) signifie que le client a envoyé trop de requêtes dans un intervalle de temps donné, au-delà de la limite de débit (rate limit) fixée par le serveur.

Usages typiques :
limiter l'usage abusif d'une API (quota par clé, par compte, par IP) ;
atténuer les attaques applicatives / DoS et le scraping ;
garantir une répartition équitable des ressources (fair use).

Points de sémantique importants :
C'est une erreur client (4xx) : la requête n'a pas été traitée, mais elle est réessayable (contrairement à un 400 ou 403).
Le corps de la réponse devrait expliquer la limite dépassée (RFC 6585) ; un format lisible par machine (JSON, ou application/problem+json selon la RFC 9457) est recommandé.
Une réponse 429 ne doit pas être stockée en cache sauf indication contraire via Cache-Control.
429 ≠ 503 : le 503 indique une indisponibilité côté serveur, le 429 un dépassement imputable au client.

L'en-tête Retry-After

Défini par la RFC 9110 §10.2.3, il indique combien de temps le client doit attendre avant de réémettre la requête. Deux formats, mutuellement exclusifs :

Forme Exemple Signification

delay-seconds (entier ≥ 0) Retry-After: 120 attendre 120 s
Date HTTP (IMF-fixdate, GMT) Retry-After: Fri, 31 Dec 2024 23:59:59 GMT attendre jusqu'à cette date

À retenir :
Retry-After est optionnel sur un 429 : le client doit prévoir un comportement de repli.
Il est aussi valide avec 503, 301/302/307/308 — la logique de retry doit donc être générique.
Il est souvent complété par des en-têtes non normalisés mais très répandus : RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (draft IETF RateLimit header fields), ou les variantes X-RateLimit-.

Côté client : consommer Retry-After correctement

Détecter le statut 429 (et 503).
Lire Retry-After : tenter d'abord la conversion en entier, sinon parser la date HTTP.
Attendre la durée indiquée, avec un plafond (ne pas dormir 6 h parce que le serveur l'a demandé) et un jitter pour éviter que tous les clients repartent en même temps.
Repli si l'en-tête est absent : backoff exponentiel (1 s, 2 s, 4 s, 8 s…) plus jitter.
Borner le nombre de tentatives et remonter l'échec à l'appelant.
Ne jamais réessayer en boucle serrée : cela aggrave la saturation.

import random
import time
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

import requests

MAX_ATTEMPTS = 5
MAX_WAIT = 60.0 plafond de sécurité, en secondes

def parse_retry_after(value: str) -> float None:
"""Retourne un délai en secondes, ou None si l'en-tête est inexploitable."""
if value is None:
return None
value = value.strip()
try:
return max(0.0, float(int(value))) forme delay-seconds
except ValueError:
pass
try: forme date HTTP
target = parsedate_to_datetime(value)
if target.tzinfo is None:
target = target.replace(tzinfo=timezone.utc)
return max(0.0, (target - datetime.now(timezone.utc)).total_seconds())
except (TypeError, ValueError):
return None

def get_with_retry(url, headers=None):
for attempt in range(MAX_ATTEMPTS):
response = requests.get(url, headers=headers, timeout=10)

if response.status_code not in (429, 503):
return response

delay = parse_retry_after(response.headers.get("Retry-After"))
if delay is None: repli : backoff exponentiel
delay = 2 attempt
delay = min(delay, MAX_WAIT) + random.uniform(0, 0.5) jitter

if attempt == MAX_ATTEMPTS - 1:
break
time.sleep(delay)

response.raise_for_status()
return response

Le meilleur réessai reste celui qu'on évite : côté client, mieux vaut lisser le débit en amont (token bucket local, file d'attente, respect de RateLimit-Remaining) que subir des 429.

Côté serveur : émettre 429 + Retry-After

Le principe est toujours le même : compter les requêtes par identité (clé API, compte, IP), et lorsque le compteur dépasse la limite, répondre 429 avec un Retry-After égal au temps restant jusqu'à la réinitialisation de la fenêtre.

Outils courants :
Nginx : module limit_req (fenêtre leaky bucket) ou limit_conn.
Apache httpd : mod_qos, mod_evasive ou mod_security pour le débit de requêtes. mod_ratelimit limite la bande passante (ko/s) d'une réponse, pas le nombre de requêtes.
Node.js / Express : express-rate-limit (ajoute Retry-After et les en-têtes RateLimit-).
Python : flask-limiter, slowapi (FastAPI), ou un compteur Redis maison.
Passerelles : Kong, Envoy, API Gateway, Cloudflare — généralement la meilleure place pour cette logique.

Nginx

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
location /api/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_status 429;

Nginx n'émet pas Retry-After de lui-même : on l'ajoute explicitement.
error_page 429 = @too_many;
}

location @too_many {
add_header Retry-After 1 always;
default_type application/json;
return 429 '{"error":"too_many_requests"}';
}
}

rate=10r/s : 10 requêtes par seconde par IP ;
burst=20 : tolère une rafale de 20 requêtes ;
nodelay : sert la rafale immédiatement au lieu de l'étaler.
Il n'existe pas de variable $limit_req_delay : la valeur de Retry-After doit être fixée (ou calculée par l'application en amont).

FastAPI + slowapi

from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

headers_enabled=True active RateLimit- et Retry-After sur les 429
limiter = Limiter(key_func=get_remote_address, headers_enabled=True)

app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/endpoint")
@limiter.limit("10/minute")
async def endpoint(request: Request):
return {"message": "OK"}

Le paramètre request: Request est obligatoire pour que slowapi identifie l'appelant, et headers_enabled doit être activé : sans lui, le 429 est renvoyé sans Retry-After.

Exemple de réponse complète

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 60
Cache-Control: no-store

{
"error": "too_many_requests",
"message": "Quota de 100 requêtes/minute dépassé.",
"retry_after_seconds": 60
}

Bonnes pratiques

Documenter les limites, les fenêtres et les en-têtes renvoyés.
Fixer Retry-After sur le temps réel restant avant réinitialisation, pas sur une constante arbitraire trop grande.
Exposer les en-têtes RateLimit- pour permettre l'auto-régulation du client (et les autoriser via Access-Control-Expose-Headers en CORS).
Backoff exponentiel + jitter + plafond + nombre max de tentatives côté client.
Ne pas rejouer aveuglément des requêtes non idempotentes sans clé d'idempotence.
Superviser le taux de 429 : un pic signale soit un abus, soit une limite mal calibrée.
Granularité par identité (clé API, compte) plutôt que par IP seule, à cause du NAT et des proxys.

En résumé — Client : détecter le 429, lire Retry-After, attendre (avec plafond et jitter), réessayer un nombre borné de fois. Serveur : appliquer une limitation de débit et toujours renvoyer Retry-After avec le délai réel de réinitialisation.