What Is Laravel Cooldown?
Laravel Cooldown is a package by Mahedi Zaman Zaber that enforces per-action waiting periods in your Laravel application. While Laravel's built-in RateLimiter counts how many requests hit an endpoint within a window, Cooldown answers a different question: is this specific action still within its waiting period for this specific owner? That distinction matters for flows like OTP resends, password reset requests, or payment retries, where you care about the gap between attempts rather than a raw request count.
Key Features at a Glance
- Owner-scoped actions — scope a cooldown to a model, an IP string, or nothing for a global lock
HasCooldownstrait — attach$user->cooldown('action')directly to any Eloquent model- Route middleware —
cooldown:action,durationwith an optional driver argument - Atomic
block()— acquires a lock, runs the callback, and sets the cooldown only on success enforce()— throwsCooldownActiveException, rendered as HTTP 429CooldownInfoobject — exposesremainingSeconds()andremainingForHumans()- Cache and database backends — switchable per call; database backend survives cache flushes
- Pruning — expired database rows cleared via Laravel's
model:prunecommand
The Fluent API
A cooldown is a named action, an optional owner, and a duration:
use ZaberDev\Cooldown\Facades\Cooldown;
// Global lock for 10 minutes
Cooldown::for('rebuild_search_index')->for(600);
// User-scoped lock for 15 minutes
Cooldown::for('resend_verification', $user)->for(900);
// Lock until end of day
Cooldown::for('daily_checkin', $user)->until(now()->endOfDay());
When a cooldown is active, info() returns a CooldownInfo object:
if (Cooldown::for('resend_verification', $user)->active()) {
$info = Cooldown::for('resend_verification', $user)->info();
echo "Please wait " . $info->remainingForHumans() . " before requesting another email.";
}
To stop a request outright instead of branching, call enforce():
Cooldown::for('request_password_reset', $user)->enforce();
Atomic Blocking
Race conditions are the classic problem with cooldowns: two concurrent requests can both pass the active check before either one sets the lock. block() solves this by acquiring an atomic lock, running the callback, and only applying the cooldown if the work succeeds:
Cooldown::for('send_login_code', $user)->block(function () use ($smsClient, $user) {
$smsClient->sendCode($user->phone);
}, duration: 90);
Cooldowns on Eloquent Models
Add the HasCooldowns trait to any model and the same fluent builder becomes available directly on the instance:
use ZaberDev\Cooldown\HasCooldowns;
class User extends Authenticatable
{
use HasCooldowns;
}
$user->cooldown('change_avatar')->for(300);
if ($user->cooldown('change_avatar')->active()) {
return response()->json(['message' => 'You can change your avatar again shortly.'], 429);
}
$user->cooldown('change_avatar')->reset();
On the database backend, records are polymorphic, so you can query a model's cooldowns like any relation:
$activeCooldowns = $user->cooldowns()->where('expires_at', '>', now())->get();
Route Middleware
Route::post('/feedback', [FeedbackController::class, 'store'])
->middleware('cooldown:submit_feedback,120');
Route::post('/invoices/export', [InvoiceController::class, 'export'])
->middleware('cooldown:invoice_export,600,database');
Storage Backends
The default driver is cache (Redis or Memcached). Switch to database when the lock must survive a cache flush—billing flows being the obvious example:
Cooldown::for('renew_subscription', $user)->using('database')->for(86400);
Custom drivers can be registered via Cooldown::extend() in a service provider.
Installation
Requires PHP 8.2 and supports Laravel 11, 12, and 13:
composer require zaber-dev/laravel-cooldown
php artisan vendor:publish --provider="ZaberDev\Cooldown\CooldownServiceProvider"
php artisan migrate
Configuration lives in config/cooldown.php and covers the default driver, cache store, key prefix, table name, and event dispatching.
Real Takeaways
- Cooldown complements
RateLimiter—use it when the gap between attempts matters more than a request count. block()is the right tool for any action where a race condition could fire duplicate side effects.- The database backend is worth the extra write cost for anything tied to billing or compliance.
- The
HasCooldownstrait keeps model-scoped logic clean and avoids scatteringCooldown::for($user)calls everywhere. - Pruning expired rows with
model:prunekeeps thecooldownstable lean without custom maintenance scripts.
Source: Enforce Per-Action Waiting Periods in Laravel with Cooldown — Laravel News