Rate limiting e throttling no Laravel

Rate limiting e throttling são técnicas essenciais para controlar o tráfego de requisições em aplicações web. Embora frequentemente usados como sinônimos, eles têm diferenças sutis: rate limiting define um limite absoluto de requisições em um período (ex: 100 requisições/minuto), enquanto throttling é um mecanismo mais dinâmico que pode reduzir gradualmente a velocidade de processamento conforme o uso se aproxima do limite.

No Laravel, implementar esses controles é fundamental para proteger sua aplicação contra abusos como ataques de força bruta, scraping excessivo, DDoS e sobrecarga de recursos. O framework oferece duas ferramentas nativas poderosas: o middleware throttle para proteção rápida em rotas, e a facade RateLimiter para controle programático mais refinado.

Configuração Inicial e Middleware throttle

O middleware throttle é a forma mais simples de aplicar rate limiting. Sua sintaxe básica é throttle:60,1, que significa 60 requisições a cada 1 minuto.

// Em routes/web.php ou routes/api.php
Route::get('/api/users', function () {
    return User::all();
})->middleware('throttle:60,1');

Para grupos de rotas, aplique o middleware no grupo inteiro:

Route::middleware('throttle:100,1')->group(function () {
    Route::get('/api/posts', [PostController::class, 'index']);
    Route::post('/api/posts', [PostController::class, 'store']);
});

Você também pode usar nomes de limitadores pré-definidos. O Laravel já inclui api e login como limitadores padrão:

// Aplica o limitador 'api' configurado no AppServiceProvider
Route::middleware('throttle:api')->group(function () {
    // ...
});

Rate Limiter com a Facade RateLimiter

Para controle mais granular, use a facade Illuminate\Support\Facades\RateLimiter. Seus métodos principais incluem:

  • attempt(): Executa uma ação se o limite não foi atingido
  • tooManyAttempts(): Verifica se o limite foi excedido
  • hit(): Incrementa o contador de tentativas
  • clear(): Reseta o contador para uma chave específica

Exemplo prático: limitar envio de e-mails por usuário a 5 por hora:

use Illuminate\Support\Facades\RateLimiter;

public function sendEmail(Request $request)
{
    $key = 'send-email:' . $request->user()->id;

    $executed = RateLimiter::attempt(
        $key,
        $maxAttempts = 5,
        function() use ($request) {
            // Lógica de envio de e-mail
            Mail::to($request->user())->send(new WelcomeMail());

            return response()->json(['message' => 'E-mail enviado']);
        },
        $decaySeconds = 3600 // 1 hora
    );

    if (! $executed) {
        $seconds = RateLimiter::availableIn($key);

        return response()->json([
            'message' => 'Muitas tentativas. Tente novamente em ' . $seconds . ' segundos.',
            'retry_after' => $seconds
        ], 429);
    }

    return $executed;
}

Limitadores Nomeados e Definição no AppServiceProvider

Registre limitadores personalizados no método boot() do AppServiceProvider usando RateLimiter::for():

// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot()
{
    RateLimiter::for('uploads', function (Request $request) {
        return Limit::perMinute(10)
            ->by($request->user()?->id ?: $request->ip());
    });

    RateLimiter::for('login', function (Request $request) {
        return Limit::perMinute(5)
            ->by($request->input('email') . '|' . $request->ip())
            ->response(function () {
                return response('Muitas tentativas de login.', 429);
            });
    });
}

Para usar esses limitadores nas rotas:

Route::post('/upload', [UploadController::class, 'store'])
    ->middleware('throttle:uploads');

Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:login');

Segmentação por Usuário e IP

Uma das vantagens dos limitadores nomeados é a segmentação dinâmica. O método by() define como identificar cada cliente. Exemplo: 100 requisições/min para usuários logados, 10 para anônimos:

RateLimiter::for('api', function (Request $request) {
    $user = $request->user();

    if ($user) {
        return Limit::perMinute(100)->by($user->id);
    }

    return Limit::perMinute(10)->by($request->ip());
});

Respostas Personalizadas e Headers HTTP

Por padrão, o Laravel retorna uma resposta 429 com JSON. Para personalizar, use o método response() no limitador:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->response(function (Request $request, array $headers) {
        return response('Limite excedido. Tente novamente mais tarde.', 429, $headers);
    });
});

Para incluir headers HTTP de rate limiting, o Laravel já adiciona automaticamente X-RateLimit-Limit, X-RateLimit-Remaining e Retry-After. Você pode acessá-los na resposta:

$response = response()->json(['message' => 'OK']);
$response->header('X-RateLimit-Limit', 60);
$response->header('X-RateLimit-Remaining', RateLimiter::remaining($key));
return $response;

Para tratamento avançado com exceções:

use Illuminate\Http\Exceptions\ThrottleRequestsException;

public function handleRateLimit(Request $request, Closure $next)
{
    if (RateLimiter::tooManyAttempts($key, 5)) {
        throw new ThrottleRequestsException('Muitas requisições');
    }

    return $next($request);
}

Testes e Cache de Rate Limiting

Em testes, use RateLimiter::clear() para resetar os contadores entre cenários:

public function test_rate_limit()
{
    RateLimiter::clear('send-email:' . $user->id);

    // Simula 5 requisições
    for ($i = 0; $i < 5; $i++) {
        $response = $this->post('/send-email');
        $response->assertStatus(200);
    }

    // A 6ª deve ser bloqueada
    $response = $this->post('/send-email');
    $response->assertStatus(429);
}

Para desempenho em produção, configure o cache driver para Redis em vez de file:

CACHE_DRIVER=redis
REDIS_CLIENT=predis

O Redis oferece operações atômicas e melhor performance para contadores de rate limiting em alta concorrência.

Boas Práticas e Casos de Uso Avançados

Limitação por endpoint específico: Endpoints críticos como login e upload devem ter limites mais restritivos:

Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1'); // 5 tentativas por minuto

Route::post('/upload-avatar', [UploadController::class, 'avatar'])
    ->middleware('throttle:3,10'); // 3 uploads a cada 10 minutos

Integração com filas: Para operações lentas que disparam jobs, use rate limiting para evitar sobrecarga no processamento:

public function processBatch()
{
    $key = 'batch-process:' . auth()->id();

    if (RateLimiter::tooManyAttempts($key, 2)) {
        return back()->with('error', 'Processamento já está em andamento.');
    }

    RateLimiter::hit($key, 60);
    ProcessBatchJob::dispatch();
}

Estratégias de backoff exponencial: Combine rate limiting com notificações ao usuário:

if (RateLimiter::tooManyAttempts($key, 5)) {
    $seconds = RateLimiter::availableIn($key);
    $waitMinutes = ceil($seconds / 60);

    Log::warning('Rate limit excedido para usuário ' . $user->id);
    $user->notify(new RateLimitExceededNotification($waitMinutes));

    return response('Limite excedido. Tente novamente em ' . $waitMinutes . ' minutos.', 429);
}

Implementar rate limiting e throttling no Laravel não é apenas uma questão de segurança, mas também de qualidade de serviço. Com as ferramentas nativas do framework, você pode proteger sua aplicação de forma elegante e eficiente, garantindo que recursos sejam distribuídos de maneira justa entre todos os usuários.

Referências