Cooldown en Laravel: Controla Tiempos de Espera por Acción
Introdución
En aplicaciones modernas, proteger tus APIs y funcionalidades contra el abuso es fundamental. Un usuario malintencionado podría enviar múltiples solicitudes en segundos, causando problemas de rendimiento o consumiendo recursos innecesarios. Cooldown en Laravel es una solución elegante que implementa períodos de espera por acción, permitiendo que definas cuánto tiempo debe pasar entre cada ejecución de una acción específica.
A diferencia del rate limiting genérico, Cooldown trabaja a nivel de acción individual y por propietario (usuario, IP, o lo que definas). Es ideal para casos como:
- Envío de correos electrónicos de verificación
- Reintentos de autenticación fallida
- Publicación de comentarios o posts
- Solicitudes de cambio de contraseña
- Creación de recursos limitados
En este artículo, exploraremos cómo instalarlo, configurarlo y usarlo en tus proyectos Laravel.
¿Qué es Cooldown y por qué lo necesitas?
Cooldown es un paquete Laravel que enforza períodos de espera entre acciones. A diferencia del middleware de rate limiting que actúa globalmente, Cooldown opera a nivel granular: por acción y por propietario.
Imagina que quieres:
- Permitir que un usuario envíe un email de verificación solo una vez cada 5 minutos
- Limitar intentos de login fallidos a uno cada 10 segundos
- Permitir crear un post cada 2 minutos como máximo
Cooldown maneja esto de manera fluida con:
- API fluida para definición intuitiva
- Trait de Eloquent para integración directa en modelos
- Middleware de ruta para protección automática
- Almacenamiento flexible en caché o base de datos
- Bloqueos atómicos para evitar condiciones de carrera
Instalación y Configuración
Primero, instala el paquete con Composer:
composer require spatie/laravel-cooldown
Opcionalmente, publica la configuración:
php artisan vendor:publish --provider="Spatie\Cooldown\CooldownServiceProvider"
Esto crea un archivo config/cooldown.php donde puedes personalizar el almacenamiento (caché o base de datos) y otros parámetros.
Uso básico con la API fluida
El uso más simple es dentro de un controlador o comando:
<?php
namespace App\Http\Controllers;
use Spatie\Cooldown\Facades\Cooldown;
use Illuminate\Http\Request;
class EmailVerificationController extends Controller
{
public function sendVerificationEmail(Request $request)
{
$user = $request->user();
// Verifica si el usuario está en cooldown
if (Cooldown::for('send-verification-email')
->owner($user->id)
->isActive()) {
return response()->json([
'message' => 'Debes esperar antes de enviar otro email',
'retry_after' => Cooldown::for('send-verification-email')
->owner($user->id)
->remainingSeconds()
], 429);
}
// Envía el email
Mail::send(new VerificationMail($user));
// Activa el cooldown por 5 minutos
Cooldown::for('send-verification-email')
->owner($user->id)
->activateFor(minutes: 5);
return response()->json(['message' => 'Email enviado']);
}
}
En este ejemplo:
for('send-verification-email')define la acciónowner($user->id)especifica a quién aplica el cooldownisActive()verifica si está activoremainingSeconds()obtiene segundos restantesactivateFor()inicia el período de espera
Integración con modelos Eloquent
Usa el trait HasCooldowns en tus modelos:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Cooldown\Traits\HasCooldowns;
class Post extends Model
{
use HasCooldowns;
}
Ahora puedes trabajar directamente con la instancia:
$post = auth()->user()->posts()->create($validated);
// Verifica cooldown
if ($post->cooldown('create-post')
->owner(auth()->id())
->isActive()) {
return response()->json(['error' => 'Espera antes de crear otro post'], 429);
}
// Activa cooldown de 2 minutos
$post->cooldown('create-post')
->owner(auth()->id())
->activateFor(minutes: 2);
Middleware de ruta
Protege rutas automáticamente con middleware:
<?php
Route::post('/posts', [PostController::class, 'store'])
->middleware(\Spatie\Cooldown\Http\Middleware\Cooldown::class . ':create-post,5');
Route::post('/send-email', [EmailController::class, 'send'])
->middleware(\Spatie\Cooldown\Http\Middleware\Cooldown::class . ':send-email,3');
El middleware automáticamente:
- Verifica el cooldown
- Retorna 429 Too Many Requests si está activo
- Activa el cooldown después de la acción
Casos de uso avanzados
Cooldown con diferentes propietarios
A veces necesitas múltiples propietarios (usuario + IP, por ejemplo):
Cooldown::for('api-request')
->owner(auth()->id())
->owner(request()->ip())
->isActive();
Obtener información del cooldown
$cooldown = Cooldown::for('send-verification-email')
->owner($user->id);
// Verificar estado completo
if ($cooldown->isActive()) {
$remaining = $cooldown->remainingSeconds();
$expires = $cooldown->expiresAt();
return response()->json([
'status' => 'cooldown_active',
'retry_after' => $remaining,
'expires_at' => $expires
], 429);
}
Restablecimiento manual
// Elimina el cooldown activo
Cooldown::for('send-verification-email')
->owner($user->id)
->reset();
// Ahora puede ejecutar la acción nuevamente
En Jobs y Queues
Integra cooldown en tareas en segundo plano:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Spatie\Cooldown\Facades\Cooldown;
class SendNotificationEmail implements ShouldQueue
{
public function __construct(
public int $userId
) {}
public function handle()
{
// Verifica antes de procesar
if (Cooldown::for('notification-email')
->owner($this->userId)
->isActive()) {
return; // Skip si está en cooldown
}
// Procesa la notificación
// ...
// Activa cooldown de 1 hora
Cooldown::for('notification-email')
->owner($this->userId)
->activateFor(hours: 1);
}
}
Almacenamiento en base de datos
Por defecto, Cooldown usa caché. Para usar base de datos, modifica config/cooldown.php:
return [
'driver' => 'database',
];
Crea la tabla con migración:
php artisan make:migration create_cooldowns_table
<?php
Schema::create('cooldowns', function (Blueprint $table) {
$table->id();
$table->string('action');
$table->string('owner');
$table->timestamp('expires_at');
$table->timestamps();
$table->unique(['action', 'owner']);
});
Respuestas HTTP amigables
Implementa una respuesta consistente:
<?php
namespace App\Http\Controllers;
use Spatie\Cooldown\Facades\Cooldown;
class ApiController extends Controller
{
protected function checkCooldown(string $action, $owner, int $minutes = 5)
{
if (Cooldown::for($action)->owner($owner)->isActive()) {
$remaining = Cooldown::for($action)
->owner($owner)
->remainingSeconds();
return response()->json([
'message' => 'Acción muy frecuente',
'retry_after' => $remaining,
], 429);
}
}
protected function activateCooldown(string $action, $owner, int $minutes = 5)
{
Cooldown::for($action)
->owner($owner)
->activateFor(minutes: $minutes);
}
}
Úsalo así:
public function store(Request $request)
{
if ($response = $this->checkCooldown('create-resource', auth()->id())) {
return $response;
}
// Lógica de creación...
$this->activateCooldown('create-resource', auth()->id(), minutes: 2);
return response()->json(['success' => true]);
}
Testing con Cooldown
Desactiva cooldown en tests:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Spatie\Cooldown\Facades\Cooldown;
class CooldownTest extends TestCase
{
public function test_cooldown_prevents_duplicate_action()
{
$user = User::factory()->create();
// Primera solicitud: éxito
$response = $this->actingAs($user)->post('/send-email');
$response->assertOk();
// Segunda inmediata: bloqueada
$response = $this->actingAs($user)->post('/send-email');
$response->assertStatus(429);
}
public function test_cooldown_expires()
{
$user = User::factory()->create();
Cooldown::fake(); // Desactiva en tests si es necesario
}
}
Integración con autenticación
Protege intentos de login:
<?php
namespace App\Http\Controllers\Auth;
use Spatie\Cooldown\Facades\Cooldown;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
public function store(Request $request)
{
$email = $request->email;
// Verifica cooldown por IP e email
if (Cooldown::for('login-attempt')
->owner($email)
->isActive()) {
throw ValidationException::withMessages([
'email' => 'Demasiados intentos. Intenta más tarde.'
]);
}
if (!Auth::attempt($request->only('email', 'password'))) {
// Activa cooldown después de fallo
Cooldown::for('login-attempt')
->owner($email)
->activateFor(minutes: 5);
throw ValidationException::withMessages([
'email' => 'Credenciales inválidas.'
]);
}
return redirect('/dashboard');
}
}
Puntos clave
- Cooldown implementa límites de tiempo por acción y propietario, no globales
- Usa API fluida para verificar y activar períodos de espera
- El trait HasCooldowns integra cooldown directamente en modelos Eloquent
- Middleware automático protege rutas sin código adicional
- Almacena en caché o base de datos según necesidades
- Múltiples propietarios permiten límites combinados (usuario + IP)
- remainingSeconds() y expiresAt() facilitan respuestas amigables al cliente
- Útil para autenticación, APIs, notificaciones y creación de recursos
- En tests, desactiva cooldown para evitar bloqueos en suites
- Previene abuso, spam y carga excesiva de forma elegante