laravel/framework · error · DeadlockException
{$e->getMessage()}
Error message
{$e->getMessage()} What it means
DeadlockException thrown by the transaction-retry loop in ManagesTransactions when a concurrency-induced error (deadlock/serialization failure) occurs inside a nested transaction (transactions > 1). Because the whole MySQL transaction is rolled back by the server on deadlock, the framework cannot retry in place and must propagate so the caller can restart the outermost transaction.
Source
Thrown at src/Illuminate/Database/Concerns/ManagesTransactions.php:101
* @param int $maxAttempts
* @return void
*
* @throws \Throwable
*/
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
// On a deadlock, MySQL rolls back the entire transaction so we can't just
// retry the query. We have to throw this exception all the way out and
// let the developer handle it in another way. We will decrement too.
if ($this->causedByConcurrencyError($e) &&
$this->transactions > 1) {
$this->transactions--;
$this->transactionsManager?->rollback(
$this->getName(), $this->transactions
);
throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e);
}
// If there was an exception we will rollback this transaction and then we
// can check if we have exceeded the maximum attempt count for this and
// if we haven't we will return and try this query again in our loop.
$this->rollBack();
if ($this->causedByConcurrencyError($e) &&
$currentAttempt < $maxAttempts) {
return;
}
throw $e;
}
/**
* Start a new database transaction.
*View on GitHub (pinned to e0f6eb3518)
Solutions
- Catch DeadlockException at the outermost transaction boundary and retry the entire unit of work with backoff.
- Reduce transaction scope and duration to lower deadlock probability.
- Ensure consistent lock ordering across concurrent paths (always lock tables/rows in the same order).
- Add missing indexes so row locks replace gap/table locks, shrinking the contention surface.
- Increase retry attempts on the outer transaction (the second arg to transaction()) if appropriate.
Example fix
// before
DB::transaction(function () {
DB::transaction(function () { /* nested work */ }); // deadlock bubbles out
});
// after - retry the outermost unit
use Illuminate\Support\retry;
use Illuminate\Database\QueryException;
retry(
function () { DB::transaction(function () { /* full unit of work */ }); },
attempts: 5,
sleepMilliseconds: 100,
when: fn ($e) => $e instanceof QueryException && $e->isDeadlock()
); Defensive patterns
Strategy: retry
Validate before calling
// There is no pre-call validation for a deadlock; instead structure the unit
// to be safely retried. Use retry() around the outermost transaction.
use Illuminate\Support\retry;
retry(
fn () => DB::transaction(fn () => $this->performWork()),
$maxAttempts = 5,
$sleepMs = 100,
fn (\Throwable $e) => $e instanceof \Illuminate\Database\DeadlockException
|| ($e instanceof \Illuminate\Database\QueryException && $e->isDeadlock())
); Type guard
function isDeadlock(\Throwable $e): bool
{
if ($e instanceof \Illuminate\Database\DeadlockException) return true;
return $e instanceof \Illuminate\Database\QueryException && $e->isDeadlock();
} Try / catch
use Illuminate\Database\DeadlockException;
use Illuminate\Support\retry;
retry(
function () use ($payload) {
DB::transaction(fn () => $this->process($payload));
},
attempts: 5,
sleepMilliseconds: 100,
when: fn (\Throwable $e) => $e instanceof DeadlockException
); Prevention
- Acquire locks (FOR UPDATE) in a consistent global order across all code paths.
- Keep transactions short and avoid user/network I/O inside them.
- Add indexes so locks are row-scoped, not gap/table-scoped.
- Catch DeadlockException at the outermost boundary and retry the whole unit with jittered backoff.
When it happens
Trigger: Two concurrent requests acquire locks in opposite orders and one is chosen as the deadlock victim inside a nested DB::transaction; long-running nested transactions increasing lock contention; SELECT ... FOR UPDATE patterns that deadlock under load.
Common situations: High-concurrency writes touching the same rows; missing indexes widening lock scope; long transactions holding locks; oversized transaction granularity; retry budget exhausted at the outer level so the inner deadlock surfaces.
Related errors
- Transactions Manager has not been set.
- Flushing locks is only supported when the lock store is sepa
- Due to PHP limitations, the fork driver may not be used with
- Please install the "spatie/fork" Composer package in order t
- Concurrent process failed with exit code [%s]. Message: %s
AI-assisted analysis of laravel/framework@e0f6eb3518 (2026-08-11).
Data as JSON: /api/errors/8936678eb1916843.
Report an issue: GitHub.