A lock wait can be healthy coordination or an expensive queue. PostgreSQL’s lock_timeout lets an operation stop waiting, but it does not diagnose why the holder is slow or decide whether retrying is appropriate. Choose the timeout from the operation’s latency budget, and inspect the blocking transaction before increasing the limit or multiplying attempts.
When it breaks
lock_timeout applies separately to each lock acquisition attempt, including implicit locks. It measures time waiting for that lock, not all time spent by the statement. A query already doing useful work can still hit the limit when it later requests another lock. Several shorter waits can also accumulate beyond one configured interval. Treating this as a whole-query deadline leaves that accumulation uncontrolled.
statement_timeout bounds statement execution, including lock waiting. If it is shorter, it can cancel the statement before the lock timeout fires. idle_in_transaction_session_timeout addresses a different problem: a session waiting for its next client command while keeping a transaction open. A backend blocked inside an update is ordinarily active, with a lock wait event; calling every blocked connection idle in transaction directs diagnosis at the wrong sessions.
Check it quickly
Open two fresh psql -X -v ON_ERROR_STOP=1 sessions, A and B, against the same disposable PostgreSQL 18 database. Start with the default lock_timeout of zero and no conflicting timeout settings. The short durations below make the demonstration fast; they are not production recommendations. A creates the fixture:
CREATE SCHEMA timeout_demo;
CREATE TABLE timeout_demo.counter (id integer PRIMARY KEY,n integer);
INSERT INTO timeout_demo.counter VALUES (1,0);
A updates the row inside an open transaction and keeps its lock:
BEGIN;
UPDATE timeout_demo.counter SET n=10 WHERE id=1;
B waits at most one hundred milliseconds for its conflicting update. Temporarily allowing expected errors also exposes the transaction state before rollback:
BEGIN;
SET LOCAL lock_timeout='100ms';
SET LOCAL statement_timeout='1s';
\set ON_ERROR_STOP off
UPDATE timeout_demo.counter SET n=n+1 WHERE id=1;
\echo lock_state :SQLSTATE
SELECT n FROM timeout_demo.counter WHERE id=1;
\echo transaction_state :SQLSTATE
\set ON_ERROR_STOP on
ROLLBACK;
Expect lock_state 55P03, then transaction_state 25P02. The first error cancels this statement and leaves the explicit transaction failed. B’s rollback clears that failed state; it does not release A’s lock or undo A’s pending update. Repeating work inside B’s failed transaction would only generate another transaction-state error.
Leave A open. B now gives the whole statement a shorter budget than the lock acquisition:
BEGIN;
SET LOCAL lock_timeout='100ms';
SET LOCAL statement_timeout='50ms';
\set ON_ERROR_STOP off
UPDATE timeout_demo.counter SET n=n+1 WHERE id=1;
\echo statement_state :SQLSTATE
\set ON_ERROR_STOP on
ROLLBACK;
This returns statement_state 57014. The operation is still waiting for the same row, but the statement deadline wins. These error classes are evidence to interpret with context: 55P03 can also arise from NOWAIT, and 57014 also covers other query cancellations. Neither code by itself proves a deadlock or names the blocking application.
In A, discard the pending value ten and release the row:
ROLLBACK;
B starts a fresh, bounded transaction and succeeds:
BEGIN;
SET LOCAL lock_timeout='100ms';
SET LOCAL statement_timeout='1s';
UPDATE timeout_demo.counter SET n=n+1 WHERE id=1 RETURNING n;
COMMIT;
The returned value is one. Neither failed B attempt committed an increment, and A rolled back its ten. Next, B demonstrates that ordinary elapsed execution does not consume the lock-wait budget:
BEGIN;
SET LOCAL lock_timeout='50ms';
SET LOCAL statement_timeout='1s';
SELECT pg_sleep(0.2);
\echo sleep_state :SQLSTATE
COMMIT;
SHOW lock_timeout;
The two-hundred-millisecond sleep succeeds with sleep_state 00000 despite a fifty-millisecond lock timeout. After commit, SHOW returns the session’s original zero because SET LOCAL ended with its transaction. A verifies the counter remains one and cleans up:
SELECT n FROM timeout_demo.counter WHERE id=1;
DROP SCHEMA timeout_demo CASCADE;
Recover without amplifying contention
Select retry policy per operation. A lock timeout may justify a new transaction after rollback, provided the request still has time and replay is valid. A small attempt cap, jitter, and an overall deadline bound the extra load; they cannot make a permanently blocked resource available. Keep the original error and report exhausted attempts instead of silently dropping the request.
A true deadlock is a cycle of dependencies. PostgreSQL detects it and reports 40P01; a lock timeout can fire before that detection, so a timed-out run does not establish that no cycle existed. Inspect blocker identities, transaction age, and lock acquisition order. Shortening a transaction that waits for external input often removes the cause more effectively than raising every caller’s timeout. Do not copy these laboratory millisecond limits into an unrelated workload without measuring normal hold times.
Sources
- PostgreSQL 18 client connection defaults, lock, statement, and idle transaction timeout scopes, checked 2026-09-10.
- PostgreSQL 18 explicit locking, conflicting locks and deadlock detection, checked 2026-09-10.