Handling Database Deadlocks in Laravel with Exponential Backoff
Overview
Database deadlocks occur when two or more transactions hold locks that the other requires, creating a standstill. In high-concurrency
Prerequisites
To implement this technique, you should be comfortable with
Key Libraries & Tools
- Laravel: The primary PHP framework providing the
DBfacade. - Laravel: Used for database interactions and record locking.
- Laravel: The specific exception class caught to detect deadlock messages.

Code Walkthrough
The logic centers on a while(true) loop that attempts the transaction until it succeeds or hits a retry limit.
$attempts = 0;
$maxRetries = 5;
while (true) {
try {
DB::transaction(function () use ($passengerId, $amount) {
// Lock records for update to prevent concurrent race conditions
$wallet = Wallet::where('user_id', $passengerId)->lockForUpdate()->first();
// Perform multiple database operations
$wallet->debit($amount);
$passenger = Passenger::lockForUpdate()->find($passengerId);
$passenger->update(['status' => 'paid']);
});
break; // Success! Exit the loop.
} catch (QueryException $e) {
$attempts++;
if ($attempts >= $maxRetries || !str_contains($e->getMessage(), 'Deadlock found')) {
throw $e; // Give up or fail on non-deadlock errors
}
// Wait longer after each failure: 100ms, 200ms, 400ms, etc.
usleep(100 * 1000 * pow(2, $attempts - 1));
}
}
Explanation
- Locking:
lockForUpdate()prevents other sessions from modifying these rows until the transaction commits. - The Catch: We specifically look for the "Deadlock found" string within the exception message.
- Exponential Backoff: We use
usleepto pause execution. By doubling the wait time each time, we give the database breathing room to clear existing locks.
Syntax Notes
Notice the use of while(true) combined with an internal break. This pattern is cleaner than complex conditional loops when you need to exit immediately upon a successful DB::transaction. The pow(2, $attempts - 1) function creates the exponential growth of the sleep duration.
Practical Examples
This pattern is vital for Wallet Systems where a user might trigger multiple API calls simultaneously (e.g., refreshing a payment page). It is also highly effective for Inventory Management during flash sales, where hundreds of transactions compete for the same product stock rows.
Tips & Gotchas
Avoid over-engineering; if your application handles low traffic, standard transactions are usually enough. Always set a strict $maxRetries to prevent infinite loops. If a different error occurs (like a syntax error), the code is designed to throw $e immediately rather than retrying, saving CPU cycles.