deadlock_timeout

deadlock_timeout sets how long a backend waits on a lock BEFORE it bothers to check whether a deadlock has formed.

MEDIUM RISK
Safe first move

Default 1s is right for almost everyone — change it only with a measured reason.

Tier 1 — Lab-Proven — Measured behavior and safety boundaries are available below.

Measured Effect

summary

MEASURED that the time to DETECT a real deadlock tracks deadlock_timeout exactly. A genuine two-session row deadlock was built (A holds row1 then waits row2; B holds row2 then waits row1 — a hard cycle); a poller watched for deadlock detected and recorded the interval from when A began waiting to when a victim was aborted.

observations

deadlock timeoutscenariotime to detectoutcome
1sreal A<->B deadlock1.09s after A began waitingERROR: deadlock detected, victim aborted
4ssame A<->B deadlock4.03s after A began waitingERROR: deadlock detected, victim aborted
1sNON-deadlock wait (A holds row1 ~3s, no cycle)n/a — no cyclewaiter NOT aborted at 1s; waited the full ~3s and then acquired the lock (proves the dial schedules the CHECK, not the grant)

session survives

A deadlock victim's transaction is rolled back but the SESSION SURVIVES — ERROR: deadlock detected is a non-fatal statement error (like statement_timeout/lock_timeout, and unlike the FATAL idle_*_timeout family which terminate the connection). The other transaction in the cycle proceeds. The server log records ERROR: deadlock detected.

reading

It is a WAIT-BEFORE-CHECK dial, not a cancel timeout. The detector runs only after a backend has waited deadlock_timeout on a lock; PostgreSQL defers it because nearly all lock waits clear well before 1s and the wait-for-graph scan costs CPU. For a genuine deadlock the detection time — and therefore how long the cycle stalls before recovery — equals deadlock_timeout. For an ordinary wait the dial does nothing visible: the statement simply waits for the lock as long as it needs.

honesty note

deadlock_timeout is a server-side timer; the chosen seconds are lab values, but the mechanism (defer the deadlock scan until a wait exceeds the dial; abort one victim non-fatally on a true cycle; do nothing for a non-cycle wait) is deterministic and box-independent. Detection time is measured around a real cycle, so the ~1.0s/~4.0s figures are the dial itself, not machine speed.

Reproduced Danger

Sizing / Safe Range

Workload Guidance

Unlock Incident Pro

The free sections tell you what to do. Pro is the measured proof — the exact failure signature, safe-ceiling formula, and per-workload defaults, each captured on real PG 14–18 runs.

  • Reproduced Danger
  • Sizing / Safe Range
  • Workload Guidance
Unlock Incident Pro

deadlock_timeout sets how long a backend waits on a lock BEFORE it bothers to check whether a deadlock has formed. It is NOT a timeout that cancels anything by itself: when a statement blocks on a lock, PostgreSQL starts this timer, and only if the wait lasts longer than deadlock_timeout does the backend run the (relatively expensive) deadlock-detection algorithm that walks the wait-for graph. If a true cycle is found, ONE transaction in the cycle is aborted with ERROR: deadlock detected (a non-fatal error — the session survives, the transaction rolls back); if there is no cycle, the backend simply keeps waiting for the lock as normal. The default is 1s. The design assumption is that the vast majority of lock waits clear on their own in far less than a second, so paying for a deadlock scan on every brief wait would be wasteful — the timer defers the scan until a wait is suspiciously long. Three facts set it apart from the statement/lock/idle timeout family: (1) its category is Lock Management, not Statement Behavior; (2) context=SUPERUSER (a normal user cannot change it for their session, unlike statement_timeout/lock_timeout which are context=user); and (3) min_val=1, so it canNOT be set to 0 / disabled — deadlock detection is always on, you only tune WHEN it runs. It exists unchanged on PG 14-18 (no version cliff).

name

deadlock_timeout

setting default

1000 (1s)

unit

ms

vartype

integer

category

Lock Management

context

superuser

min val

1

max val

2147483647

enumvals

(n/a — integer ms)

boot val

1000

reset val

1000

pending restart

false

short desc

Sets the time to wait on a lock before checking for deadlock.

ground truth note

Captured literally from pg_settings on PG 14-18 (-18) on 2026-06-20. setting=1000, boot_val=1000, reset_val=1000 on EVERY version (default_changed=false). context=SUPERUSER (NOT user — a non-superuser session cannot SET it). vartype=integer, unit=ms, min_val=1 (canNOT be 0 — detection cannot be disabled), max_val=2147483647. category='Lock Management', short_desc='Sets the time to wait on a lock before checking for deadlock.' Identical across 14, 15, 16, 17, 18.

Common Pitfalls

  • Treating it as a 'cancel a stuck query' timeout — it is not; it only schedules the deadlock CHECK. To bound a wait, use lock_timeout; to bound run time, statement_timeout.
  • Lowering it to 'detect deadlocks faster' on a busy system — it does nothing for ordinary waits and adds a deadlock-graph scan to every wait that crosses the shorter threshold (CPU under contention).
  • Raising it to 'save CPU' without realizing it lengthens every REAL deadlock outage by the same amount (the cycle is frozen until the timer fires).
  • Assuming a normal user can SET it per session — it is context=SUPERUSER; non-superusers cannot change it.
  • Trying to disable deadlock detection by setting 0 — rejected (min=1); detection is always on.
  • Tuning deadlock_timeout instead of fixing the cause — inconsistent lock ordering and long transactions cause deadlocks; address those plus lock_timeout first.
  • Forgetting it also gates log_lock_waits — changing deadlock_timeout shifts when lock waits get logged.

Evidence and References

  • PostgreSQL docs: Runtime Config — Lock Management (deadlock_timeout — the amount of time to wait on a lock before checking for a deadlock; set high enough that ordinary lock waits do not trigger the check; also the threshold for log_lock_waits; requires superuser to change).
  • PostgreSQL docs: Concurrency Control — Deadlocks (PostgreSQL detects deadlocks automatically and aborts one transaction with ERROR: deadlock detected; the application should retry; prevent them with consistent lock ordering).
  • PostgreSQL docs: lock_timeout and statement_timeout (bound the WAIT for a lock and the RUN time of a statement respectively — the levers to use alongside, or instead of, tuning the deadlock detector).
  • PostgreSQL docs: log_lock_waits (logs a lock wait that exceeds deadlock_timeout — the recommended visibility lever).
  • Lab capture 2026-06-20: — a real A<->B row deadlock was detected 1.09s after the wait began under deadlock_timeout=1s and 4.03s under 4s ; EDGE1 the same deadlock stayed frozen 1.07s at 1s vs 5.04s at 5s (recovery time = the dial), EDGE2 a non-deadlock waiter under deadlock_timeout=1s was not aborted at 1s and waited the full 3.01s until the holder released ; boundary min=1 (0 rejected as out of range), context=superuser, identical /; pg_settings ground truth PG 14-18 (-18).

Technical Reference

Category
Lock Management
Type
integer (ms (milliseconds; minimum 1 — it canNOT be disabled))
Blast radius
session
Severity
Moderate
Context
superuser
Versions
14, 15, 16, 17, 18
Reviewed
2026-06-20

Severity scale: 1 Minimal · 2 Low · 3 Moderate · 4 High · 5 Critical.

Keep going

Related & next steps