Resilient database transactions for Laravel applications that need to gracefully handle deadlocks, serialization failures, and any other transient database errors you configure. This helper wraps DB::transaction() with targeted retries, structured logging, and exponential backoff so you can keep your business logic simple while surviving temporary contention.
- Retries known transient failures out of the box (SQLSTATE
40001, MySQL driver errors1213and1205), and lets you add extra SQLSTATE codes, driver error codes, or exception classes through configuration. - Exponential backoff with jitter between attempts to reduce stampedes under load.
- Structured logs with request metadata, SQL, bindings, connection information, and stack traces written to dated files under
storage/logs/{Y-m-d}. - Log titles include the exception class and codes, making it easy to see exactly what triggered the retry.
- Optional transaction labels and custom log file names for easier traceability across microservices and jobs.
- Convenience
DB::transactionWithRetrymacro on both the facade and individual connections so existing transaction code stays readable. - Laravel package auto-discovery; no manual service provider registration required.
composer require ahed92wakim/laravel-db-transaction-retryThe package ships with the DatabaseTransactionRetryServiceProvider, which Laravel auto-discovers. No additional setup is needed.
use DatabaseTransactions\RetryHelper\Services\TransactionRetrier as Retry;
$order = Retry::runWithRetry(
function () use ($payload) {
$order = Order::create($payload);
$order->logAuditTrail();
return $order;
},
maxRetries: 4,
retryDelay: 1,
logFileName: 'database/transaction-retries/orders',
trxLabel: 'order-create'
);runWithRetry() returns the value produced by your callback, just like DB::transaction(). If every attempt fails, the last exception is re-thrown so your calling code can continue its normal error handling.
Prefer working through the database facade? Call the included transactionWithRetry macro and keep identical behaviour and parameters:
$invoice = DB::transactionWithRetry(
function () use ($payload) {
return Invoice::fromPayload($payload);
},
maxRetries: 5,
retryDelay: 1,
trxLabel: 'invoice-sync'
);Need connection-specific logic? Because the macro is applied to Illuminate\Support\Facades\DB and to every resolved Illuminate\Database\Connection, you can call it on connection instances as well:
$report = DB::connection('analytics')->transactionWithRetry(
fn () => $builder->lockForUpdate()->selectRaw('count(*) as total')->first(),
trxLabel: 'analytics-rollup'
);The macro is registered automatically when the service provider boots, and sets the tx.label container binding the same way as the helper.
| Parameter | Default | Description |
|---|---|---|
maxRetries |
Config (default: 3) |
Total number of attempts (initial try + retries). |
retryDelay |
Config (default: 2s) |
Base delay (seconds). Actual wait uses exponential backoff with ±25% jitter. |
logFileName |
Config (default: database/transaction-retries) |
Written to storage/logs/{Y-m-d}/{logFileName}.log. Can point to subdirectories. |
trxLabel |
'' |
Optional label injected into log titles and stored in the service container as tx.label for downstream consumers. |
Call the helper anywhere you would normally open a transaction—controllers, jobs, console commands, or domain services.
Publish the configuration file to tweak defaults globally:
php artisan vendor:publish --tag=database-transaction-retry-config-
Key options (
config/database-transaction-retry.php): -
max_retries,retry_delay, andlog_file_nameset the package-wide defaults when you omit parameters. Each respects environment variables (DB_TRANSACTION_RETRY_MAX_RETRIES,DB_TRANSACTION_RETRY_DELAY,DB_TRANSACTION_RETRY_LOG_FILE). -
lock_wait_timeout_secondslets you overrideinnodb_lock_wait_timeoutper attempt; set the matching environment variable (DB_TRANSACTION_RETRY_LOCK_WAIT_TIMEOUT) to control the session value or leave null to use the database default. -
logging.channelpoints at any existing Laravel log channel so you can reuse stacks or third-party drivers. -
logging.levels.success/logging.levels.failurelet you tune the severity emitted for successful retries and exhausted attempts (defaults:warninganderror). -
retry_on_deadlocktoggles the built-in handling for MySQL deadlocks (1213). SetDB_TRANSACTION_RETRY_ON_DEADLOCK=falseto disable it. -
retry_on_lock_wait_timeouttoggles retries for MySQL lock wait timeouts (1205) and activates the optional session timeout override. SetDB_TRANSACTION_RETRY_ON_LOCK_WAIT_TIMEOUT=trueto enable it.
Retries are attempted when the caught exception matches one of the configured conditions:
Illuminate\Database\QueryExceptionfor MySQL deadlocks (1213) whenretry_on_deadlockis enabled (default).Illuminate\Database\QueryExceptionfor MySQL lock wait timeouts (1205) whenretry_on_lock_wait_timeoutis enabled.
Everything else (e.g., constraint violations, syntax errors, application exceptions) is surfaced immediately without logging or sleeping. If no attempt succeeds and all retries are exhausted, the last exception is re-thrown. In the rare case nothing is thrown but the loop exits, a RuntimeException is raised to signal exhaustion.
When lock_wait_timeout_seconds is configured, the retrier issues SET SESSION innodb_lock_wait_timeout = {seconds} on the active connection before each attempt, but only when retry_on_lock_wait_timeout is enabled. This keeps the timeout predictable even after reconnects or pool reuse, and on drivers that do not support the statement the helper safely ignores the failure.
By default, logs are written using a dedicated single-file channel per day. Override logging.channel to integrate with your own logging stack:
- Success after retries → a warning entry titled
"[trxLabel] [DATABASE TRANSACTION RETRY - SUCCESS] ExceptionClass (Codes) After (Attempts: x/y) - Warning". - Failure after exhausting retries → an error entry titled
"[trxLabel] [DATABASE TRANSACTION RETRY - FAILED] ExceptionClass (Codes) After (Attempts: x/y) - Error".
Each log entry includes:
- Attempt count, maximum retries, and transaction label.
- Exception class, SQLSTATE, driver error code, connection name, SQL, resolved raw SQL, and PDO error info when available.
- A compacted stack trace and sanitized bindings.
- Request URL, method, authorization header length, and authenticated user ID when the request helper is bound.
Set logFileName to segment logs by feature or workload (e.g., logFileName: 'database/queues/payments').
Use the built-in Artisan commands to temporarily disable or re-enable retries without touching configuration files:
php artisan db-transaction-retry:stop # disable retries
php artisan db-transaction-retry:start # enable retriesThe commands write a small marker file inside the package (storage/runtime/retry-disabled.marker). As long as that file exists retries stay off; removing it or running db-transaction-retry:start brings them back. You can still set the DB_TRANSACTION_RETRY_ENABLED environment variable for a permanent default.
Heads up: The
db-transaction-retry:startcommand only removes the disable marker—it does not override an explicitdatabase-transaction-retry.enabled=falseconfiguration (including theDB_TRANSACTION_RETRY_ENABLED=falseenvironment variable). Update that setting totrueif you want retries to remain enabled after the current process.
The package exposes dedicated support classes you can reuse in your own instrumentation:
DatabaseTransactions\RetryHelper\Support\TransactionRetryLogWriterwrites structured entries using the same format as the retrier.DatabaseTransactions\RetryHelper\Support\TraceFormatterconverts debug backtraces into log-friendly arrays.DatabaseTransactions\RetryHelper\Support\BindingStringifiersanitises query bindings before logging.
For testing scenarios, the retrier looks for a namespaced DatabaseTransactions\RetryHelper\sleep() function before falling back to PHP's global sleep(), making it easy to assert backoff intervals without introducing delays.
Run the test suite with:
composer testTests cover the retry flow, logging behaviour, exponential backoff jitter, and non-retryable scenarios using fakes for the database and logger managers.
- PHP
>= 8.2 - Laravel
>= 11.0
Bugs, ideas, and pull requests are welcome. Feel free to open an issue describing the problem or improvement before submitting a PR so we can collaborate on scope.
This package is open-sourced software released under the MIT License.