# Maîtriser le throttling DRF : pourquoi c’est nécessaire et comment le configurer, l’appliquer et le personnaliser Limiter le trafic avec `limit_req` dans un reverse proxy nginx est efficace, mais lorsqu’on veut appliquer des politiques différentes par vue/action (ex. login 5/min, upload 20/jour, API de recherche 60/min), compter sur la couche serveur devient fragile. Le throttling de DRF permet de créer des limites *au niveau de l’application* adaptées aux caractéristiques de chaque endpoint, ce qui en fait une compétence indispensable. --- ## Pourquoi le throttling est indispensable Le throttling de DRF décide, comme les permissions, si une requête est autorisée, mais il s’agit d’une restriction *temporaire* (fréquence) plutôt que permanente. Le guide officiel le décrit comme un état temporaire qui contrôle la vitesse à laquelle un client peut appeler l’API. Les besoins courants sont les suivants : * **Atténuation des abus/attaques** : force brute, spam, crawling, DoS simple * **Protection des coûts/ressources** : uploads, appels à des API externes, requêtes lourdes, appels à des modèles génératifs * **Équité (fair use)** : empêcher qu’un utilisateur ou une clé monopolise les ressources * **Codification des politiques** : gérer des règles comme "5 requêtes/min" dans le code plutôt que dans l’infrastructure Le throttling peut être appliqué globalement ou par vue/action, ce qui dépasse souvent les possibilités d’un seul proxy. --- ## Comment fonctionne le throttling DRF (concepts clés) ### 1) Chaîne de taux (Rate string) DRF utilise des chaînes comme `"100/day"` ou `"60/min"` pour définir les limites. ### 2) Qui est limité (identification du client) * `UserRateThrottle` : limite par ID utilisateur si authentifié, sinon par IP * `AnonRateThrottle` : limite uniquement les requêtes anonymes par IP * `ScopedRateThrottle` : applique une politique par *scope* (ex. `uploads`, `login`) L’identification IP se fait via `X-Forwarded-For` ou `REMOTE_ADDR`. Si vous êtes derrière un proxy, configurez `NUM_PROXIES` correctement. ### 3) Stockage de l’état : cache Les compteurs sont stockés dans le backend cache de Django. Sur un seul processus, `LocMemCache` suffit, mais dans un environnement multi‑workers ou multi‑replicas, un cache partagé comme Redis est indispensable. --- ## Configuration globale : démarrage rapide ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "100/day", "user": "1000/day", }, } ``` Cette configuration applique les règles par défaut à l’ensemble de l’API. En cas de dépassement, DRF renvoie automatiquement un **HTTP 429 (Too Many Requests)**. --- ## Appliquer le throttling à une vue : règles différentes par endpoint ### 1) Vue basée sur classe (APIView) ```python from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.throttling import UserRateThrottle class ExpensiveView(APIView): throttle_classes = [UserRateThrottle] def get(self, request): return Response({"ok": True}) ``` ### 2) Vue fonctionnelle (@api_view) ```python from rest_framework.decorators import api_view, throttle_classes from rest_framework.throttling import UserRateThrottle from rest_framework.response import Response @api_view(["GET"]) @throttle_classes([UserRateThrottle]) def ping(request): return Response({"pong": True}) ``` ### 3) Action spécifique d’un ViewSet (@action) ```python from rest_framework.decorators import action from rest_framework.throttling import UserRateThrottle from rest_framework.viewsets import ViewSet from rest_framework.response import Response class ItemViewSet(ViewSet): @action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle]) def purchase(self, request, pk=None): return Response({"purchased": pk}) ``` Les throttles définis sur une action prévalent sur ceux du niveau ViewSet. --- ## Utiliser ScopedRateThrottle pour des politiques par vue (recommandé) Le throttling par scope permet de nommer les politiques de façon significative, ce qui rend la gestion plus propre. ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.ScopedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "login": "5/min", "uploads": "20/day", "search": "60/min", }, } ``` Dans la vue, il suffit de déclarer le scope : ```python from rest_framework.views import APIView from rest_framework.response import Response class LoginView(APIView): throttle_scope = "login" def post(self, request): return Response({"ok": True}) ``` DRF applique `ScopedRateThrottle` uniquement aux vues qui déclarent `throttle_scope`, en créant une clé unique basée sur le scope + ID utilisateur ou IP. --- ## Créer un throttle personnalisé : l’essentiel est la clé Les throttles intégrés suffisent souvent, mais certains cas exigent une personnalisation. * Limiter le login par combinaison IP + nom d’utilisateur * Limiter par clé API * Limiter par en-tête/tenant/organisation * Utiliser un cache différent (ex. cluster Redis) ### 1) Hériter de SimpleRateThrottle ```python from rest_framework.throttling import SimpleRateThrottle class LoginBurstThrottle(SimpleRateThrottle): scope = "login" def get_cache_key(self, request, view): username = (request.data.get("username") or "").lower().strip() ident = self.get_ident(request) # IP if not username: return None return f"throttle_login:{ident}:{username}" ``` Ajoutez le scope dans les paramètres : ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "path.to.LoginBurstThrottle", ], "DEFAULT_THROTTLE_RATES": { "login": "5/min", }, } ``` ### 2) Utiliser un cache différent ```python from django.core.cache import caches from rest_framework.throttling import AnonRateThrottle class CustomCacheAnonThrottle(AnonRateThrottle): cache = caches["alternate"] ``` --- ## Points à retenir avant le déploiement ### 1) Problème d’IP identique derrière un proxy Si `NUM_PROXIES` n’est pas correctement configuré, toutes les requêtes peuvent être vues comme provenant du même IP. ### 2) LocMemCache n’est pas fiable en multi‑workers Chaque worker possède son propre cache local, ce qui peut entraîner des compteurs incohérents. Utilisez un cache partagé comme Redis. ### 3) Condition de concurrence (race condition) Les throttles intégrés peuvent laisser passer quelques requêtes supplémentaires en haute concurrence. Pour des cas critiques (paiement, coupon), envisagez une implémentation atomique (Redis INCR + EXPIRE). ### 4) Expérience client : 429 et Retry‑After DRF renvoie 429 par défaut. En implémentant `wait()` dans votre throttle, vous pouvez ajouter l’en‑tête `Retry-After` pour informer le client quand il peut réessayer. --- ## Conclusion : combinez nginx et DRF throttling ![Robot Club entry restriction and 429 neon sign](/media/editor_temp/6/c96bb1d6-7cde-48a0-98aa-07396570391d.png) * **nginx** : filtre le trafic massif et les attaques dès la première couche * **DRF throttling** : applique des règles fines basées sur le sens et le coût de chaque endpoint En particulier, limiter chaque vue selon ses spécificités est la force de DRF : la logique reste dans le code, indépendante de l’infrastructure.