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

  1. Catch DeadlockException at the outermost transaction boundary and retry the entire unit of work with backoff.
  2. Reduce transaction scope and duration to lower deadlock probability.
  3. Ensure consistent lock ordering across concurrent paths (always lock tables/rows in the same order).
  4. Add missing indexes so row locks replace gap/table locks, shrinking the contention surface.
  5. 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

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


AI-assisted analysis of laravel/framework@e0f6eb3518 (2026-08-11). Data as JSON: /api/errors/8936678eb1916843. Report an issue: GitHub.