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

applications—such as those processing financial transactions or passenger fees—these errors are common. This tutorial demonstrates an elegant retry strategy that wraps
Laravel
in a loop with exponential backoff, ensuring temporary locking issues don't crash the user experience.

Prerequisites

To implement this technique, you should be comfortable with

and
Laravel
fundamentals. You need an understanding of
Laravel
and how
Laravel
handles model updates. Familiarity with try-catch blocks and basic loop control is essential.

Key Libraries & Tools

  • Laravel
    : The primary PHP framework providing the DB facade.
  • Laravel
    : Used for database interactions and record locking.
  • Laravel
    : The specific exception class caught to detect deadlock messages.
Handling Database Deadlocks in Laravel with Exponential Backoff
Laravel DB Transaction with Try-Catch for Deadlocks

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

  1. Locking: lockForUpdate() prevents other sessions from modifying these rows until the transaction commits.
  2. The Catch: We specifically look for the "Deadlock found" string within the exception message.
  3. Exponential Backoff: We use usleep to 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.

3 min read