What Is Laravel Lock?
Race conditions in queue workers are easy to overlook until a customer receives two identical shipments. Laravel's built-in Cache::lock() handles the primitive, but you still have to format the key, manage the owner token, and remember the finally block every time. Laravel Lock, by Md Mahedi Zaman Zaber, wraps all of that behind a fluent builder, a model trait, and route middleware.
Acquiring and Releasing Locks
The Lock facade accepts an action name and an optional target, then returns a pending lock you can configure before acquiring:
use ZaberDev\Lock\Facades\Lock;
$lock = Lock::for('shipment_dispatch', $shipment)->ttl(120);
if ($lock->acquire()) {
try {
$carrier->dispatch($shipment);
} finally {
$lock->release();
}
}
Each builder generates its own UUID owner token. A second Lock::for(...) instance carries a different token, so calling release() on it does nothing — preventing accidental cross-process releases. When the acquire and release happen in separate processes, set the token explicitly with ->owner('worker-7').
For a cleaner one-liner, block() acquires, runs the callback, and releases automatically:
$manifest = Lock::for('shipment_dispatch', $shipment)
->block(function () use ($shipment, $carrier) {
return $carrier->dispatch($shipment);
});
If the lock is already held, block() throws LockAcquisitionException — which carries the LockInfo of the blocking lock — rather than silently skipping the work. Both acquire() and block() accept a wait duration so they retry before giving up:
$lock->acquire(blockSeconds: 5);
Lock::for('stock_allocation', $warehouse)->block($callback, 60, 5);
Model-Scoped Locks with HasLocks
Add the HasLocks trait to any Eloquent model and the lock target is derived automatically from the morph class and primary key:
use ZaberDev\Lock\HasLocks;
class Shipment extends Model
{
use HasLocks;
}
$shipment->lock('dispatch')->ttl(120)->acquire();
$shipment->isLocked('dispatch');
$shipment->forceReleaseLock('dispatch');
The generated key looks like dispatch:App_Models_Shipment:42. Register a morph map and you get the shorter alias. Non-model targets can implement the Lockable interface and return a custom identifier string.
Route Middleware
The package registers a lock middleware alias. Pass it an action name and a TTL in seconds:
Route::post('/warehouse/reconcile', [ReconcileController::class, 'store'])
->middleware('lock:warehouse_reconcile,300');
To scope the lock to a specific route model binding, embed the parameter in the action name:
Route::post('/shipments/{shipment}/dispatch', [ShipmentController::class, 'dispatch'])
->middleware('lock:shipment_dispatch:{shipment},60');
When the lock is already held, the middleware throws LockAcquisitionException before the controller runs. The exception code is 423, but you need to handle it explicitly to return the right HTTP status:
$exceptions->render(function (LockAcquisitionException $e) {
return response()->json([
'message' => 'Already processing. Try again in a moment.',
'retry_after' => $e->lockInfo?->remainingSeconds(),
], 429);
});
Cache vs. Database Storage
The default driver is set via LOCK_DRIVER. Switch per lock with ->using('database'):
- Cache driver — uses
Cache::add()for atomicity; fast and suitable for Redis or Memcached. - Database driver — writes to a
lockstable withlockForUpdate()inside a transaction; survives cache flushes and supports Eloquent queries.
Expired rows are pruned via Laravel's Prunable trait. Schedule it daily:
Schedule::command('model:prune', ['--model' => LockModel::class])->daily();
Three events fire when enabled: LockAcquired, LockFailed, and LockReleased. Listening for LockFailed surfaces which actions actually contend in production.
Installation
Requires PHP 8.2+ and Laravel 11, 12, or 13:
composer require zaber-dev/laravel-lock
php artisan vendor:publish --tag=locks-config
php artisan vendor:publish --tag=locks-migrations
php artisan migrate
Key Takeaways
- Fluent builder with
acquire(),block(),refresh(), and inspection helpers likeremainingSeconds(). HasLockstrait auto-generates model-scoped lock keys using morph class and primary key.- Route middleware protects endpoints or individual model routes without touching controller code.
- Two drivers: fast cache-backed locks or durable database locks that survive restarts.
LockAcquisitionExceptioncarriesLockInfoso callers know how long to wait before retrying.
Source: Laravel Lock: Distributed Locks for Models and Routes — Laravel News