lock_timeout
Maximum time to wait for a lock. Default 0 = wait forever. Too low: cancels legitimate DDL. Too high: lock-queue pileup blocks all readers.
Tier 1 — Lab-Proven — Measured behavior and safety boundaries are available below.
What We Observed
mechanism
lock_timeout cancels any lock request that waits longer than the timeout. Differs from statement_timeout: only fires during lock acquisition, not during query execution.
from lt effect
blocker holds ACCESS EXCLUSIVE on lt_demo: AccessExclusiveLock|t
Waiter tries SELECT (needs ACCESS SHARE, conflicts with the held ACCESS EXCLUSIVE):
waiter lock_timeout=2s | CANCELED (lock timeout) | waited=2.3s (lock_timeout=0 would wait forever; not run here to avoid hanging the lab.)
Now release the blocker; the waiter is no longer blocked and acquires the lock:
waiter lock_timeout=2s | acquired count=1000 | waited=0.4s
Session survives the cancel? (cancel on lock wait, then run SELECT 1 in the SAME session)
ERROR: canceling statement due to lock timeout
session still alive: 1
(lock_timeout bounds the WAIT for a lock with a non-fatal ERROR; the session keeps working. It does NOT bound work done AFTER the lock is acquired -- that is statement_timeout.)
live verification
- 1000ms lock
CANCELED at timeout
- 30000ms
allowed to complete
- 0 no limit
waits forever
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
lock_timeout sets the maximum time a statement will WAIT to acquire a lock before giving up. If the statement cannot get a lock it needs within the limit, the backend cancels it with a NON-FATAL error — ERROR: canceling statement due to lock timeout — aborts that statement (and its transaction), but the SESSION stays connected. The default is 0, meaning a statement will wait indefinitely for a lock. Crucially, lock_timeout bounds ONLY the time spent waiting for a lock, NOT the time the statement runs once it has the lock (that is statement_timeout). Its highest-value use is protecting schema changes: an ALTER TABLE (or any DDL) that needs an ACCESS EXCLUSIVE lock will wait behind any open transaction, and while it waits it sits at the head of the lock queue and blocks every new query on that table — so a single migration stuck behind a slow transaction can freeze all reads and writes of the table. Setting a small lock_timeout on the migration makes it fail fast and retry instead of holding the queue and taking the table down. The value can be set per session, role, database, or cluster-wide, and takes effect immediately because context=user.
- name
lock_timeout
- setting default
0 (disabled — wait forever)
- unit
ms
- vartype
integer
- category
Client Connection Defaults / Statement Behavior
- context
user
- min val
0
- max val
2147483647
- enumvals
(n/a — integer ms)
- boot val
0
- reset val
0
- pending restart
false
- short desc
Sets the maximum allowed duration of any wait for a lock.
- ground truth note
Captured literally from pg_settings on PG 14-18 (-18) on 2026-06-20. setting=0, boot_val=0, reset_val=0 on EVERY version (default_changed=false) — the guard ships DISABLED (wait forever). context=user (session-changeable, no reload/restart). vartype=integer, unit=ms, min_val=0 (0 disables), max_val=2147483647. category='Client Connection Defaults / Statement Behavior', short_desc='Sets the maximum allowed duration of any wait for a lock.' Identical across 14, 15, 16, 17, 18.
Rock-stable across versions: default=0 (disabled), min=0, max=2147483647, context=user, vartype=integer, unit=ms on PG 14, 15, 16, 17 and 18 — no version cliff (default_changed=false). Its siblings in the timeout family: statement_timeout (bounds total RUN time of a statement) and idle_in_transaction_session_timeout are present and unchanged across 14-18; transaction_timeout (total transaction duration) is NEW in PG 17. Per-version detail in _guc_matrix_machine.
context=user: set it per session with SET lock_timeout = '2s'; (effective immediately), most usefully as SET LOCAL lock_timeout = '...' at the start of a migration transaction, or per role/database with ALTER ROLE/DATABASE . SET, or cluster-wide via postgresql.conf / ALTER SYSTEM + reload. CONTEXT PROOF (captured literal, identical on and): context=user, so a session SET is accepted with no wrong-context error. BOUNDARY PROOF (captured literal, identical on and): a negative value is rejected at set time -- ALTER SYSTEM SET lock_timeout = -1; -> ERROR: -1 ms is outside the valid range for parameter "lock_timeout" (0 ms .. 2147483647 ms). The valid range is 0 . 2147483647 ms; 0 DISABLES the cap (wait forever, the default). The value is in milliseconds but accepts unit suffixes like '2s', '500ms', '5min'.
Managed providers (Azure Database for PostgreSQL Flexible Server, Amazon RDS / Aurora, Cloud SQL, Crunchy, Supabase, Neon, Heroku) expose lock_timeout as a server parameter, default 0, and because context=user you can set it per-session/role/database and via SET LOCAL in a migration without provider involvement. Schema-migration tools (and several frameworks) set a short lock_timeout automatically around DDL for exactly the pileup reason; if yours does not, wrap migrations yourself. Recommended posture: a small lock_timeout (1-5s) with retries for the deploy/migration path, looser or unset for application and reporting roles, paired with statement_timeout for run-time protection and transaction_timeout (PG 17+) for total-transaction bounds. Do not leave migration tooling at the default 0.
Common Pitfalls
- Leaving lock_timeout at 0 for online DDL — a blocked ALTER TABLE camps at the head of the lock queue and freezes every read and write on the table.
- Assuming lock_timeout caps how long a statement RUNS — it caps only the WAIT for a lock; a statement that acquires its lock then runs long is unbounded (use statement_timeout).
- Setting a tight lock_timeout globally — ordinary statements that hit normal brief contention start failing with 'canceling statement due to lock timeout'.
- Forgetting that a long DDL still blocks the table while it HOLDS the lock — lock_timeout shortens the wait, not the hold; keep DDL transactions short.
- Not retrying after a lock_timeout cancel on a migration — the whole point is fail-fast-and-retry; without retry the migration just errors out.
- Confusing it with deadlock_timeout — that controls when the deadlock detector wakes, not how long a statement waits for a non-deadlocked lock.
Evidence and References
- PostgreSQL docs: Runtime Config — Client Connection Defaults / Statement Behavior (lock_timeout — abort any statement that waits longer than the limit to acquire a lock; 0 disables; bounds the wait, not the run time; related statement_timeout, deadlock_timeout).
- PostgreSQL docs: Explicit Locking / table-level lock modes and the lock conflict matrix (why an ACCESS EXCLUSIVE request queues, and why later compatible requests queue behind it — the basis of the pileup).
- PostgreSQL docs: pg_locks and pg_stat_activity (wait_event_type=Lock) — diagnosing a lock-queue pileup (one ungranted strong lock with requests queued behind it).
- PostgreSQL docs: statement_timeout and transaction_timeout (the run-time and transaction-duration caps that complement lock_timeout's wait-only scope; transaction_timeout new in PG 17).
- Lab capture 2026-06-20: — a waiter canceled at ~2.3s under lock_timeout=2s while a blocker held ACCESS EXCLUSIVE, session survived ; a queued ALTER TABLE froze a compatible SELECT (statement_timeout=3s hit) until given lock_timeout=1s, after which reads flowed, and lock_timeout=1s did NOT cancel a 4s pg_sleep that statement_timeout=1s did ; boundary min=0 disables, range error on -1, context=user, identical /; pg_settings ground truth PG 14-18 (-18).
Technical Reference
- Category
- Client Connection Defaults / Statement Behavior
- Type
- integer (ms (milliseconds; 0 disables))
- Blast radius
- query
- Severity
- Moderate
- Context
- user
- Versions
- 14, 15, 16, 17, 18
- Reviewed
- 2026-06-20
Severity scale: 1 Minimal · 2 Low · 3 Moderate · 4 High · 5 Critical.