deadlock_timeout
deadlock_timeout sets how long a backend waits on a lock BEFORE it bothers to check whether a deadlock has formed.
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 timeout | scenario | time to detect | outcome |
|---|---|---|---|
| 1s | real A<->B deadlock | 1.09s after A began waiting | ERROR: deadlock detected, victim aborted |
| 4s | same A<->B deadlock | 4.03s after A began waiting | ERROR: deadlock detected, victim aborted |
| 1s | NON-deadlock wait (A holds row1 ~3s, no cycle) | n/a — no cycle | waiter 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
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.
Rock-stable across versions: default=1000 (1s), min=1, max=2147483647, context=superuser, vartype=integer, unit=ms on PG 14, 15, 16, 17 and 18 — no version cliff (default_changed=false). Note this is a Lock Management knob and differs from the Statement-Behavior timeout family in two ways that hold on every version: context=superuser (not user) and min=1 (cannot be disabled, unlike the statement/lock/idle timeouts whose min=0 disables them). Per-version detail in _guc_matrix_machine.
context=SUPERUSER: it can be set with SET deadlock_timeout = '2s'; ONLY by a superuser session (a normal user gets a permissions error); more commonly it is set cluster-wide in postgresql.conf / ALTER SYSTEM + reload, or per role/database via ALTER ROLE/DATABASE . SET by a superuser. CONTEXT PROOF (captured literal, identical on and): context=superuser; our postgres (superuser) connection's SET is accepted with no wrong-context error — but the operative caveat is that a non-superuser cannot change it for their own session. BOUNDARY PROOF (captured literal, identical on and): the minimum is 1 ms and it canNOT be disabled -- ALTER SYSTEM SET deadlock_timeout = 0; -> ERROR: 0 ms is outside the valid range for parameter "deadlock_timeout" (1 ms .. 2147483647 ms). The valid range is 1 . 2147483647 ms. The value is in milliseconds but accepts unit suffixes like '2s', '500ms', '5s'.
Managed providers (Azure Database for PostgreSQL Flexible Server, Amazon RDS / Aurora, Cloud SQL, Crunchy, Supabase, Neon, Heroku) expose deadlock_timeout as a server parameter with the standard 1s default. Because context=superuser and managed services restrict superuser, you typically change it through the provider's parameter group / server-parameters UI (cluster-wide) rather than a session SET; per-role/database overrides also require the elevated/admin role the provider grants. Recommended posture: leave it at 1s. It is rarely the right knob to touch on a managed instance — if you see deadlocks, fix lock-ordering and transaction length and set lock_timeout (context=user, freely settable) to bound waits; if you see deadlock-detection CPU, that is unusual and worth provider support before raising the value (which lengthens recovery). Enable log_lock_waits to surface long lock waits (logged at deadlock_timeout) for diagnosis.
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.