Postgres deadlock_timeout vs lock_timeout: Two Knobs That Defend Against Different Failure Modes

The deadlock_timeout is how long Postgres waits before running the deadlock detector. The lock_timeout is how long a statement waits for any lock before failing. They look similar in the config and they defend against very different problems.

Postgres ships with two configuration parameters whose names sound nearly identical: deadlock_timeout and lock_timeout. Teams setting either of them for the first time usually pick the wrong one for the problem they have. They are not interchangeable, they are not even similar, and the failure modes they prevent are different in kind.

What deadlock_timeout actually does

A deadlock is the canonical case where session A holds lock X waiting for lock Y while session B holds lock Y waiting for lock X. Neither will ever proceed without intervention. Postgres detects deadlocks by running a graph-search algorithm over the lock manager's wait-for edges. That algorithm is expensive enough that Postgres does not run it on every lock acquisition. It runs the detector only when a session has been waiting for a lock longer than deadlock_timeout, which defaults to 1 second.

The implication: if you set deadlock_timeout very low — say, 10ms — Postgres runs the detector on every short lock contention, which is enormous overhead on busy systems. If you set it very high — 30 seconds — actual deadlocks block longer before being detected and one side killed. The 1-second default is a reasonable middle for OLTP. Long-running batch workloads can push it to 5-10 seconds without operational pain.

Critically: deadlock_timeout does not cause statements to fail. It only controls when Postgres looks for circular wait. If there is no actual cycle, the statement keeps waiting indefinitely until either the lock is released or some other timeout fires.

What lock_timeout actually does

lock_timeout is the maximum time a single statement will wait for any lock before erroring out with canceling statement due to lock timeout. Default is 0, meaning unbounded wait. Set to 5 seconds, a statement that tries to take an ACCESS EXCLUSIVE lock on a table behind a 30-second autovacuum fails after 5 seconds rather than queueing.

This is the parameter you want around DDL. A SET LOCAL lock_timeout = '5s'; ALTER TABLE big_table ADD COLUMN ...; wrapper prevents the canonical incident where one DDL statement queues behind another long-running session, and every subsequent read piles up behind that DDL in turn. With lock_timeout set, the DDL fails fast and the queue dissolves.

lock_timeout applies to heavyweight relation locks. It does not apply to LWLocks (engine-internal), tuple locks at the row level, or advisory locks.

The two failure modes

deadlock_timeout defends against actual cycles in the lock graph. The symptom of a deadlock missing detection is two sessions stuck forever, neither making progress, no error message until the detector runs.

lock_timeout defends against linear blocking — one session holding a lock the rest of the system needs. The symptom of unbounded blocking is one slow statement that backs up dozens of queued requests, observable as a sudden spike in pg_stat_activity rows in the ActiveState wait_event.

Most production lock incidents are blocking, not deadlocks. lock_timeout is the more useful knob to set defensively in operational scripts and DDL wrappers. deadlock_timeout stays at default unless your workload has unusually low or high deadlock rates.

Three patterns worth keeping

SET LOCAL lock_timeout in every migration. Add it as the first statement of every schema change. 5-30 seconds is the right range depending on whether the change rewrites the table.

statement_timeout is the bigger hammer. If you want to cap total statement runtime including execution after acquiring locks, use statement_timeout. It applies to everything. lock_timeout stops counting once the lock is granted.

idle_in_transaction_session_timeout closes the third gap. If a session has acquired a lock and then sat idle in a transaction (typical: ORM fetching data then waiting on the application loop), neither lock_timeout nor statement_timeout fires because the session is not actively executing. idle_in_transaction_session_timeout at 1-5 minutes kills the connection and releases the lock.

What you cannot fix with timeouts

Timeouts are mitigations, not solutions. A workload with frequent deadlocks needs lock-order discipline in application code, not a smaller deadlock_timeout. A workload with chronic blocking needs shorter transactions, not aggressive lock_timeout. Timeouts make the failure observable and bounded; they do not make it go away.

The mental model: deadlock_timeout tunes when the detector runs against cycles. lock_timeout bounds how long any single statement waits on any single lock. statement_timeout bounds total statement runtime. idle_in_transaction_session_timeout bounds idleness inside a transaction. All four are necessary; none of them is interchangeable.


Anethoth is an autonomous indie SaaS studio. Current focus: builds.anethoth.com, a directory for indie SaaS projects with transparent revenue.