Skip to main content
← Back to articles
Debugging note Laravel production issue and solution

Fixing Database Deadlocks in Laravel Queue Workers: Root Causes, Locking & Retry Strategies (2026)

Learn how to eliminate SQL 1213 database deadlocks in parallel Laravel queue workers using lock ordering, DB::transaction retries, and afterCommit dispatches.

Birendra Jung Rai 4 min read
Fixing Database Deadlocks in Laravel Queue Workers: Root Causes, Locking & Retry Strategies (2026)

Production debugging note

This article explains the real cause of the issue, the correct fix, and the checks that help prevent the same problem from returning.

When scaling background queues across multiple Supervisor processes, MySQL and PostgreSQL deadlock errors like SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock start surfacing in your failed jobs table.

This guide explores why deadlocks emerge under parallel queue execution and outlines concrete architectural patterns to resolve them.

Why Deadlocks Happen in Parallel Queue Workers

A database deadlock occurs when two concurrent transactions hold locks on separate records and simultaneously attempt to acquire a lock on the record held by the other.

Neither transaction can proceed, forcing the database engine to terminate one transaction as the deadlock victim.

In Laravel applications, deadlocks typically occur in three scenarios:

  • Unordered Bulk Updates: Worker A updates records in order [ID: 10, ID: 25], while Worker B updates [ID: 25, ID: 10] concurrently.
  • Premature Job Dispatch: A queue job begins processing a model before the parent HTTP request transaction has committed to the database.
  • Overlapping Row Locks: Multiple workers attempt to select and update the same related records (such as deducting stock or updating wallet balances) using unindexed foreign keys or range locks.

1. Enforce Consistent Lock Ordering

The most common root cause is inconsistent update order across multiple concurrent jobs.

When modifying collections of models, always sort primary keys deterministically before acquiring row-level locks:

PHP
namespace App\Jobs;

use App\Models\InventoryBatch;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;

class DeductStockJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public array $itemQuantities // [item_id => quantity]
    ) {}

    public function handle(): void
    {
        // Sort IDs deterministically to eliminate cross-locking cycles
        $itemIds = array_keys($this->itemQuantities);
        sort($itemIds);

        DB::transaction(function () use ($itemIds) {
            $batches = InventoryBatch::whereIn('id', $itemIds)
                ->orderBy('id', 'asc')
                ->lockForUpdate()
                ->get();

            foreach ($batches as $batch) {
                $batch->decrement('quantity', $this->itemQuantities[$batch->id]);
            }
        }, attempts: 5);
    }
}

2. Leverage Automatic Transaction Retries

When using DB::beginTransaction() and DB::commit() manually, Laravel does not catch deadlock exceptions automatically.

Passing a closure to DB::transaction() allows Laravel to intercept deadlock exceptions (1213 in MySQL or 40P01 in PostgreSQL) and replay the entire transaction up to the specified attempt limit:

PHP
use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    // Isolated transactional mutations
    $order->update(['status' => 'processed']);
    $wallet->decrement('balance', $order->total_cents);
}, attempts: 3);

3. Use afterCommit on Model Event Dispatches

If a controller or service dispatches a job inside a transaction, a high-speed queue worker might pull the job from Redis and attempt to read or lock records before the parent transaction finishes committing.

Always chain afterCommit() when dispatching:

PHP
// In your Controller or Service
OrderCreated::dispatch($order)->afterCommit();
You can also set this natively on the job class definition:

PHP
namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Queueable;
use Illuminate\Queue\SerializesModels;

class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public bool $afterCommit = true;

    // ...
}

4. Isolate Jobs Using Atomic Locks or Job Middleware

When multiple jobs target the same aggregate root (such as the same customer account or warehouse SKU), serialize execution before hitting the database using the WithoutOverlapping middleware:

PHP
namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;

class SettleInvoiceJob implements ShouldQueue
{
    public function __construct(public int $accountId) {}

    public function middleware(): array
    {
        // Prevents parallel execution for the same account ID across all workers
        return [(new WithoutOverlapping((string) $this->accountId))->releaseAfter(30)->expireAfter(60)];
    }

    public function handle(): void
    {
        // Business logic runs safely in single-threaded context per account
    }
}

Summary Checklist for Production Queues

  • Sort record IDs in ascending order prior to executing lockForUpdate().
  • Wrap locking logic in DB::transaction(..., attempts: 3+) instead of manual transaction flags.
  • Enable afterCommit on job dispatches occurring within database transactions.
  • Apply WithoutOverlapping middleware for critical shared-resource updates.

Need help with a similar issue?

Let’s make the next technical decision clearer.

Share the problem, the relevant error, or the current system. I can help identify the practical next step.

Request a project review

Continue reading

Related Laravel fixes

View all articles →
Birendra Jung Rai

Birendra Jung Rai

Laravel Engineer • System Architect • Technical Educator