El límite de peticiones (rate limiting) es la defensa de tu API contra el abuso y contra los errores involuntarios. Sin él, un cliente mal configurado en un bucle de reintentos degrada el servicio para todos.
throttleApi() activa el middleware throttle de Laravel para el grupo api. Usa un limitador llamado api, que defines una sola vez:
// app/Providers/AppServiceProvider.php, en boot()
RateLimiter::for('api', function (Request $request): Limit {
$consumer = $request->user();
if ($consumer === null) {
return Limit::perMinute(60)->by($request->ip());
}
return Limit::perMinute($consumer->rate_limit ?? 60)
->by((string) $consumer->id);
});
Cada consumidor tiene su propio contador. Si uno agota sus sesenta peticiones, a los demás no les afecta. El techo sale de la columna rate_limit del consumidor, con un valor por defecto, así que un socio que necesita más recibe más sin un despliegue. (Documentación de Laravel: Routing › Rate Limiting.)
El orden del que no tienes que ocuparte
El limitador lee $request->user(), así que la autenticación tiene que ejecutarse antes. Pero throttleApi() puso el throttle en el grupo, y auth:sanctum está en la ruta, lo que parece el orden equivocado.
No lo es, porque Laravel ordena los middleware según una lista de prioridad incorporada, y en ella la autenticación va antes que el throttle sea cual sea el orden en que los escribiste. Una petición sin un token válido es rechazada por auth:sanctum y nunca llega a este limitador.
Eso tiene dos consecuencias. La primera es que la rama anónima de arriba solo atiende rutas que son públicas a propósito. Rechazar una petición es trabajo de la autenticación, y un limitador que también intenta hacerlo acaba autorizando por accidente.
La segunda es fácil de pasar por alto: las peticiones con un token incorrecto no tienen ningún límite. Cada una cuesta una búsqueda en la base de datos, y nada en la aplicación le impide a un cliente enviar diez mil. No puedes arreglarlo añadiendo otro throttle a la ruta, porque la misma lista de prioridad lo ordenaría también después de la autenticación.
El lugar para ese límite está delante de la aplicación. Un servidor web puede acotar las peticiones por dirección antes de que PHP arranque, lo que es más barato que cualquier cosa que pudiera hacer Laravel y cubre todas las rutas a la vez. En Nginx:
# en http {}
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
# en el location {} que sirve la API
limit_req zone=api burst=200 nodelay;
limit_req_status 429;
Sin la última línea Nginx responde 503, que le dice al cliente algo equivocado. Fija la tasa igual o por encima de lo que se le permite a tu consumidor más activo: el Capítulo 5 deja que rate_limit suba hasta 6000 por minuto, que son 100 por segundo. Esto es un techo contra las avalanchas y los tanteos, no una cuota. El limitador por consumidor de arriba sigue siendo la cuota.
¿De quién es esa dirección?
La rama anónima del limitador depende de $request->ip(), igual que el log de peticiones de más adelante en este capítulo, y detrás de un balanceador de carga esa es la dirección del balanceador. Todos los llamantes anónimos del mundo comparten un contador.
Laravel lee la dirección real del cliente en la cabecera X-Forwarded-For solo si le dices a qué proxies creer:
// bootstrap/app.php, dentro de withMiddleware()
$middleware->trustProxies(at: ['10.0.0.0/8']);
Nombra las direcciones de tu balanceador de carga, no *. Confía en todos los proxies y estarás confiando en la cabecera misma, que cualquier cliente puede fijar en lo que quiera. (Documentación de Laravel: HTTP Requests › Configuring Trusted Proxies.)
El límite de Nginx de arriba tiene el mismo problema y necesita la misma respuesta. Detrás de un balanceador de carga, $binary_remote_addr es el balanceador, y todos los clientes del mundo comparten un contador. Las directivas set_real_ip_from y real_ip_header de Nginx son su versión de los proxies de confianza. Configúralas antes de fiarte de ese límite.
Lo que ve el cliente
Laravel añade a cada respuesta con throttle las cabeceras que necesita un cliente que se comporta correctamente:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Y cuando se alcanza el límite, un 429 con dos más:
Retry-After: 42
X-RateLimit-Reset: 1790000042
{
"message": "Too Many Attempts."
}
Nada de esto lo escribes tú. Una vez mantuve un middleware que añadía estas cabeceras a mano, y leía un contador distinto del que escribía el throttle, así que los números que informaba estaban mal. Los de Laravel están bien porque salen de lo mismo que hace el recuento.
Dónde viven los contadores
El limitador cuenta en tu caché. Con el almacén de caché database que Laravel trae por defecto, cada petición a tu API es una escritura en tu base de datos antes de haber hecho ningún trabajo. En producción, apunta la caché a Redis:
CACHE_STORE=redis
Si quieres el limitador en un almacén distinto del resto de tu caché, cache.limiter en config/cache.php lo nombra.