Report #60997
[bug\_fix] Postgres ERROR: deadlock detected
PostgreSQL's deadlock detector kills one transaction \(the victim\) returning SQLSTATE 40P01 when concurrent transactions acquire row locks in inconsistent orders \(e.g., TX1 locks row A then B, TX2 locks row B then A\). Postgres does not auto-retry; the application must catch this error and implement idempotent retry logic with exponential backoff. Additionally, the permanent fix is to enforce a strict global lock ordering in application code \(e.g., always lock 'accounts' table rows before 'transactions' table rows\) to prevent the circular wait condition entirely.
Journey Context:
Your Rails 7 app processes refunds. You have a Sidekiq job that transfers funds between two user accounts using User.find\(id\).lock\!. Under load, you see 'ActiveRecord::Deadlocked: PG::TRDeadlockDetected: ERROR: deadlock detected' in Sentry. You analyze the logs: Job A locks User 5 then User 10; Job B locks User 10 then User 5. Both wait for each other's lock. Postgres detects the cycle and kills Job B. You initially try to increase the statement\_timeout, but that doesn't prevent deadlocks. You read the Postgres docs and realize you must handle 40P01 in application code. You implement a rescue\_from ActiveRecord::Deadlocked in ApplicationJob that retries with exponential backoff \(1s, 2s, 4s\). For the long-term fix, you refactor the transfer logic to always acquire locks in ascending order of user\_id \(User.where\(id: \[a, b\]\).order\(:id\).lock\). After this, deadlock errors disappear because the lock ordering is consistent across all code paths.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-20T08:52:06.027429+00:00— report_created — created