Rate Limiting
Com limitar les peticions a la teva API amb rate limiting, el RateLimiter de Laravel i estratègies per ruta.
Què és el rate limiting?#
El rate limiting controla quantes peticions pot fer un client en un període de temps determinat. Sense rate limiting, un sol client podria consumir tots els recursos del servidor: un script mal escrit que fa milers de peticions per segon, un bot que intenta endevinar contrasenyes per força bruta, o un scraper que descarrega tota la base de dades a través de l'API.
El rate limiting protegeix l'API en tres dimensions. Primer, contra abusos intencionats com atacs de força bruta, scrapers i bots. Segon, garanteix un ús equitatiu dels recursos entre tots els clients: un sol usuari no pot monopolitzar el servidor en detriment dels altres. Tercer, actua com a xarxa de seguretat contra errors accidentals: un client amb un bug que fa peticions en bucle no pot tirar el servidor a terra.
Quan un client supera el límit configurat, Laravel retorna una resposta 429 Too Many Requests amb capçaleres que indiquen quant de temps ha d'esperar. Això permet que els clients intel·ligents implementin backoff automàtic: esperen el temps indicat i reintenten la petició.
Definir limitadors#
Els limitadors es defineixen al mètode boot del AppServiceProvider utilitzant la facade RateLimiter. Cada limitador té un nom i una closure que retorna la configuració del límit:
// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
// Límit general per a l'API: 60 peticions per minut
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(
$request->user()?->id ?: $request->ip()
);
});
}El mètode by() defineix la clau d'identificació del client. En aquest exemple, els usuaris autenticats es limiten pel seu ID i els convidats per la seva IP. Això significa que cada usuari autenticat pot fer 60 peticions per minut independentment dels altres.
Límits per tipus de ruta#
No tots els endpoints de l'API tenen les mateixes necessitats de rate limiting. Un endpoint de lectura (llistar articles) pot tolerar moltes peticions perquè és una operació lleugera, sovint cachejada. En canvi, un endpoint de pujada de fitxers consumeix molts més recursos del servidor. Un endpoint de login ha de ser especialment restrictiu per prevenir atacs de força bruta. Per això, és habitual definir diversos limitadors amb noms descriptius:
public function boot(): void
{
// General: 60/minut
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(
$request->user()?->id ?: $request->ip()
);
});
// Pujada de fitxers: 10/minut
RateLimiter::for('uploads', function (Request $request) {
return Limit::perMinute(10)->by($request->user()->id);
});
// Login: 5 intents per minut per IP
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
// Enviament de correus: 3/minut
RateLimiter::for('emails', function (Request $request) {
return Limit::perMinute(3)->by($request->user()->id);
});
// Operacions sensibles: 10/hora
RateLimiter::for('sensitive', function (Request $request) {
return Limit::perHour(10)->by($request->user()->id);
});
}Cada limitador es defineix amb un nom descriptiu que després s'utilitza al middleware throttle. Per exemple, el limitador uploads es pot aplicar a les rutes de pujada amb throttle:uploads. Això centralitza la configuració: si vols canviar el límit de pujades de 10 a 20 per minut, ho canvies en un sol lloc i afecta totes les rutes que utilitzen aquest limitador.
Límits per segment de temps#
Laravel ofereix diversos mètodes per definir el segment de temps. L'elecció del segment depèn del tipus d'operació: les peticions de lectura poden tenir límits per segon o minut, les operacions d'escriptura per minut o hora, i les operacions costoses (com enviar correus) per hora o dia:
// Per segon
Limit::perSecond(1);
// Per minut
Limit::perMinute(60);
// Per hora
Limit::perHour(1000);
// Per dia
Limit::perDay(10000);
// Personalitzat: 100 peticions cada 5 minuts
Limit::every(5 * 60, 100);Límits diferenciats per usuari#
Si l'API té plans de subscripció o diferents nivells d'accés, el rate limiting pot reflectir-ho. Un usuari del pla gratuït pot tenir 30 peticions per minut, un usuari de pagament en pot tenir 200 i un client enterprise pot tenir 500 o més. Això incentiva els plans superiors i protegeix els recursos contra un ús desproporcionat per part dels comptes gratuïts:
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if (! $user) {
return Limit::perMinute(20)->by($request->ip());
}
return match ($user->plan) {
'enterprise' => Limit::perMinute(500)->by($user->id),
'pro' => Limit::perMinute(200)->by($user->id),
'basic' => Limit::perMinute(60)->by($user->id),
default => Limit::perMinute(30)->by($user->id),
};
});Fixa't que els convidats (usuaris no autenticats) tenen un límit molt més baix que qualsevol pla de pagament. Això té sentit perquè els convidats no s'han identificat i és més difícil distingir peticions legítimes d'un bot. A més, el límit dels convidats es calcula per IP, cosa que pot ser problemàtica si molts usuaris comparteixen la mateixa IP (per exemple, en una xarxa corporativa), però és l'única opció quan no hi ha identificador d'usuari.
Límits il·limitats#
De vegades, certs tipus d'usuaris no haurien d'estar subjectes a cap límit. Per exemple, els administradors del sistema que fan tasques de manteniment o monitorització, o comptes de servei interns que necessiten accés il·limitat per a processos automatitzats:
RateLimiter::for('api', function (Request $request) {
if ($request->user()?->role === 'admin') {
return Limit::none();
}
return Limit::perMinute(60)->by(
$request->user()?->id ?: $request->ip()
);
});Múltiples límits simultanis#
Pots aplicar diversos límits alhora retornant un array. Tots els límits han de complir-se:
RateLimiter::for('api', function (Request $request) {
$key = $request->user()?->id ?: $request->ip();
return [
Limit::perMinute(60)->by($key), // Màxim 60/minut
Limit::perDay(5000)->by($key), // Màxim 5000/dia
];
});Això permet, per exemple, que un client faci 60 peticions per minut però no més de 5000 al dia.
Aplicar a rutes#
Middleware throttle#
Un cop definits els limitadors, cal aplicar-los a les rutes. El middleware throttle és el pont entre la definició del limitador i les rutes que el necessiten. Pots aplicar-lo a rutes individuals, a grups de rutes o a tot el fitxer routes/api.php:
// routes/api.php
// Aplicar el limitador 'api' a un grup
Route::middleware('throttle:api')->group(function () {
Route::apiResource('articles', ArticleController::class);
Route::apiResource('categories', CategoryController::class);
});
// Aplicar un limitador específic a una ruta
Route::post('/upload', [UploadController::class, 'store'])
->middleware('throttle:uploads');
// Combinar amb autenticació
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::apiResource('articles', ArticleController::class);
});Límit numèric directe#
En lloc de referenciar un limitador amb nom, pots especificar un límit numèric directament:
// Màxim 10 peticions per minut
Route::middleware('throttle:10,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});El format és throttle:intents,minuts. Però és recomanable utilitzar limitadors amb nom perquè són més flexibles i centralitzen la configuració.
Capçaleres de resposta#
Un bon sistema de rate limiting no només bloqueja peticions excessives: també informa el client de l'estat del seu límit a cada resposta. Això permet que els clients gestionin les seves peticions de manera intel·ligent, per exemple mostrant un avís quan s'acosten al límit o implementant cues per espaiar les peticions.
Laravel afegeix automàticament capçaleres a totes les respostes (no només quan s'excedeix el límit) per informar el client:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
Quan el límit s'excedeix, la resposta 429 inclou:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 42
X-RateLimit-Reset: 1706976000
La capçalera Retry-After indica quants segons ha d'esperar el client. X-RateLimit-Reset indica el timestamp Unix en què el límit es reinicia.
Resposta personalitzada#
La resposta 429 per defecte de Laravel és funcional però genèrica. Per a una API pública, és millor retornar una resposta personalitzada que informi clarament l'usuari de què ha passat i quan pot tornar a fer peticions. El mètode response del limitador permet definir exactament quina resposta es retorna:
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Has superat el límit de peticions. Torna a intentar-ho més tard.',
'retry_after' => $headers['Retry-After'],
], 429, $headers);
});
});Rate limiting manual#
El middleware throttle és ideal per limitar peticions HTTP a rutes senceres. Però de vegades necessites rate limiting a un nivell més granular: limitar una acció concreta dins d'un controlador, limitar l'enviament de correus, limitar la generació de codis de verificació o qualsevol altra operació que vulguis controlar independentment de les rutes.
La facade RateLimiter permet fer servir el sistema de rate limiting programàticament des de qualsevol punt del codi:
use Illuminate\Support\Facades\RateLimiter;
public function sendVerificationEmail(Request $request)
{
$key = 'verify-email:' . $request->user()->id;
if (RateLimiter::tooManyAttempts($key, 3)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'message' => "Massa intents. Torna a provar d'aquí {$seconds} segons.",
], 429);
}
RateLimiter::hit($key, 60 * 5); // Bloquejar durant 5 minuts
$request->user()->sendEmailVerificationNotification();
return response()->json([
'message' => 'Correu de verificació enviat.',
]);
}La clau ($key) identifica de manera única l'acció que vols limitar. Construeix-la combinant l'acció amb l'identificador de l'usuari o la IP. El segon paràmetre de hit() és el temps en segons durant el qual el comptador es manté actiu: un cop passat aquest temps, el comptador es reinicia automàticament.
Mètodes del RateLimiter#
// Comprovar si s'han superat els intents
RateLimiter::tooManyAttempts($key, $maxAttempts);
// Registrar un intent (amb temps de bloqueig en segons)
RateLimiter::hit($key, $decaySeconds);
// Temps restant fins que es pugui tornar a intentar
RateLimiter::availableIn($key);
// Nombre d'intents restants
RateLimiter::remaining($key, $maxAttempts);
// Netejar el comptador d'intents
RateLimiter::clear($key);
// Obtenir el nombre actual d'intents
RateLimiter::attempts($key);Un cas d'ús molt comú del rate limiting manual és protegir el login contra atacs de força bruta. La idea és que després de 5 intents fallits, el compte es bloqueja durant 60 segons. Si l'intent és correcte, el comptador es neteja immediatament. La clau combina l'email i la IP perquè un atacant no pugui bloquejar el compte d'un altre usuari fent intents fallits des d'una IP diferent:
public function login(Request $request)
{
$key = 'login:' . Str::lower($request->email) . '|' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'message' => "Compte bloquejat temporalment. Torna a provar d'aquí {$seconds} segons.",
], 429);
}
if (! Auth::attempt($request->only('email', 'password'))) {
RateLimiter::hit($key, 60);
return response()->json([
'message' => 'Credencials incorrectes.',
], 401);
}
RateLimiter::clear($key);
$token = $request->user()->createToken('api-token');
return response()->json([
'token' => $token->plainTextToken,
]);
}Emmagatzematge#
El rate limiting utilitza el driver de cache configurat a l'aplicació. Per defecte és file, però per a producció es recomana un driver més ràpid com redis o memcached:
CACHE_STORE=redisRedis és especialment adequat per al rate limiting per diverses raons. Les operacions d'increment i lectura són atòmiques, cosa que elimina condicions de carrera quan múltiples peticions arriben simultàniament. Les claus a Redis es poden configurar per expirar automàticament (TTL), cosa que s'alinea perfectament amb el concepte de "finestres de temps" del rate limiting. A més, Redis és extremadament ràpid: les operacions es resolen en microsegons, afegint una latència negligible a cada petició.
Si l'aplicació s'executa en múltiples servidors (per exemple, darrere d'un load balancer), el driver file no funciona perquè cada servidor té el seu propi sistema de fitxers i comptaria les peticions independentment. Redis, en canvi, és compartit entre tots els servidors, garantint que els límits s'apliquen globalment.