Domina el Throttling de DRF: ¿Por qué es necesario y cómo configurarlo, aplicarlo y personalizarlo?

El uso de limit_req en un proxy inverso nginx es una forma efectiva de limitar el tráfico. Sin embargo, cuando se necesita aplicar políticas distintas por vista o acción (por ejemplo, 5 inicios de sesión por minuto, 20 cargas al día, 1000 consultas por usuario), depender únicamente de la configuración del servidor puede resultar insuficiente. El throttling de DRF permite crear límites basados en las características de cada endpoint a nivel de aplicación, lo que lo convierte en una habilidad esencial.


¿Por qué necesitamos throttling?

El throttling de DRF decide si se permite o no una solicitud, al igual que los permisos, pero la diferencia es que los permisos son permanentes y el throttling es temporal (basado en la frecuencia de las solicitudes). La documentación de DRF describe el throttling como un estado temporal que controla la velocidad a la que un cliente puede enviar peticiones a la API.

Las necesidades reales se pueden resumir en:

  • Mitigación de abusos/ataques: fuerza bruta de inicio de sesión, spam, crawling, DoS simples.
  • Protección de costos y recursos: cargas, llamadas a APIs externas, consultas pesadas, llamadas a IA generativa.
  • Uso justo (fair use): evitar que un usuario o clave monopolice los recursos.
  • Codificación de políticas: reglas como "esta API permite N peticiones por minuto" se gestionan en código en lugar de infraestructura.

Además, el throttling puede aplicarse globalmente o a vistas/acciones específicas, algo que nginx por sí solo no cubre completamente.


Cómo funciona el throttling de DRF (conceptos clave)

1) Cadena de tasa

DRF suele usar formatos como "100/day" o "60/min" para establecer límites.

2) ¿A quién se aplica el límite? (identificación del cliente)

  • UserRateThrottle: si el usuario está autenticado, se basa en el ID de usuario; si no, en la IP.
  • AnonRateThrottle: solo solicita anónimos, basándose en la IP.
  • ScopedRateThrottle: aplica políticas por scope (por ejemplo, "uploads").

La identificación de IP utiliza X-Forwarded-For o REMOTE_ADDR; si hay proxies, NUM_PROXIES es crucial.

3) Almacenamiento de estado: uso de caché

El throttling de DRF almacena los contadores en el backend de caché de Django. En entornos de un solo proceso/servidor, LocMemCache funciona, pero en entornos con múltiples workers o réplicas, un caché compartido como Redis es esencial.


Configuración global: el inicio más rápido

settings.py:

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/day",
        "user": "1000/day",
    },
}

Con esto, la política predeterminada se aplica a toda la API. Cuando se alcanza el límite, DRF devuelve HTTP 429 (Too Many Requests) por defecto.


Aplicar throttling a vistas: límites por endpoint

1) Vistas basadas en clases (APIView)

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})

La documentación también indica el uso de throttle_classes para aplicar throttling a nivel de vista.

2) Vistas basadas en funciones (@api_view)

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) Aplicar a una acción específica de un ViewSet (@action)

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})

El throttling definido en la acción tiene prioridad sobre la configuración a nivel de ViewSet.


Crear políticas por scope con ScopedRateThrottle (recomendado)

El throttling por scope permite separar políticas con nombres significativos (por ejemplo, "uploads", "login").

settings.py:

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.ScopedRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "login": "5/min",
        "uploads": "20/day",
        "search": "60/min",
    },
}

En la vista, solo se declara el scope:

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 aplica ScopedRateThrottle solo a vistas con throttle_scope, creando una clave única basada en scope + ID de usuario o IP.


Crear un throttling personalizado: la clave es get_cache_key

Aunque los throttles incorporados suelen ser suficientes, en la práctica surgen requisitos como:

  • Limitar el inicio de sesión por combinación IP + nombre de usuario.
  • Limitar por API Key.
  • Limitar por encabezado/tenant/organización.
  • Usar un caché distinto (por ejemplo, un clúster Redis).

1) La forma más común: heredar de SimpleRateThrottle

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)  # identificación basada en IP
        if not username:
            return None  # si no hay username, no se aplica el throttling
        return f"throttle_login:{ident}:{username}"

Y registrar el scope en settings:

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "path.to.LoginBurstThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "login": "5/min",
    },
}

2) Cambiar el caché (atributo cache)

from django.core.cache import caches
from rest_framework.throttling import AnonRateThrottle

class CustomCacheAnonThrottle(AnonRateThrottle):
    cache = caches["alternate"]

Antes de desplegar: puntos críticos a considerar

1) Problema de IP en entornos con proxies

La identificación de IP se basa en X-Forwarded-For/REMOTE_ADDR. Si no se configura correctamente NUM_PROXIES, todos los usuarios pueden ser tratados como uno solo.

2) LocMemCache es débil en entornos multi-worker/multi-server

Un caché local puede llevar a contadores independientes por worker. En producción, un caché compartido como Redis garantiza la coherencia.

3) Condición de carrera (race condition)

El throttling incorporado puede permitir que un número limitado de solicitudes adicionales pasen en situaciones de alta concurrencia. Para casos críticos (pago, cupón), considere una implementación atómica basada en Redis (INCR + EXPIRE).

4) Amigabilidad del cliente: 429 y Retry-After

DRF devuelve 429 por defecto. Implementar wait() en el throttling permite incluir la cabecera Retry-After.


Conclusión: combina nginx y throttling de DRF

Robots Club entry restriction and 429 neon sign

  • nginx: filtra tráfico masivo y ataques en la capa más externa.
  • Throttling de DRF: aplica políticas finas basadas en el significado y costo de cada endpoint.

En particular, limitar cada vista según su naturaleza es la fortaleza de DRF, manteniendo la lógica en código incluso cuando cambia la infraestructura.