PG
PRO
55P03ERRORTier 2 — Caution✅ HIGH confidence

lock not available

Category: Object Not In Prerequisite StateVersions: All Postgres versions

What this means

A statement using NOWAIT (or lock_timeout = 0) attempted to acquire a lock that was already held by another session. Rather than waiting, Postgres immediately raises this error so the caller can decide how to proceed.

Why it happens

  1. 1SELECT ... FOR UPDATE NOWAIT on a row locked by another transaction
  2. 2LOCK TABLE ... NOWAIT when another session holds a conflicting lock on the table
  3. 3ALTER TABLE with lock_timeout set to a very short value and a conflicting lock is present
  4. 4Advisory lock acquisition with pg_try_advisory_lock returning false (different code path but same pattern)

How to reproduce

A NOWAIT lock attempt fails because another session holds the row lock.

trigger — this will ERROR
CREATE TABLE items (id INT PRIMARY KEY, status TEXT);
INSERT INTO items VALUES (1, 'pending');

-- Session 1 (holds lock):
BEGIN;
SELECT * FROM items WHERE id = 1 FOR UPDATE;

-- Session 2 (immediate failure):
SELECT * FROM items WHERE id = 1 FOR UPDATE NOWAIT; -- triggers 55P03
ERROR: could not obtain lock on row in relation "items"

Fix 1: Use SKIP LOCKED to skip contended rows

When processing a queue where any available row is acceptable (job queue pattern).

fix
SELECT * FROM items
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;

Why this works

SKIP LOCKED causes the executor to skip any row whose lock cannot be immediately acquired, rather than waiting or failing. This is the standard pattern for Postgres-backed job queues: each worker atomically claims a different row without contention.

Fix 2: Remove NOWAIT and accept blocking (with lock_timeout)

When the lock will be released quickly and a brief wait is acceptable.

fix
SET lock_timeout = '5s'; -- fail after 5 seconds instead of immediately
SELECT * FROM items WHERE id = 1 FOR UPDATE;

Why this works

Without NOWAIT, the locking statement blocks until the conflicting lock is released or lock_timeout expires. lock_timeout raises 55P03 after the specified duration, providing a bounded wait without requiring application-level retry logic.

What not to do

Busy-loop retrying NOWAIT in a tight loop

Why it's wrong: Generates excessive load on the lock manager; use SKIP LOCKED or a blocking lock with lock_timeout instead.

Version notes

Postgres 9.5+SKIP LOCKED introduced. Earlier versions required NOWAIT with application-level retry.

Sources

📚 Official docs: https://www.postgresql.org/docs/current/errcodes-appendix.html

📚 Feature docs: https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE

🔧 Source ref: src/backend/storage/lmgr/lock.c — LockAcquireExtended()

📖 Further reading: Explicit Locking

Confidence assessment

✅ HIGH confidence

Well-documented. NOWAIT and SKIP LOCKED behaviour are stable since their respective introduction versions. Edge case: lock_timeout applies to all lock acquisitions including relation-level locks from DDL, not just row locks.

See also

⚙️ This error reference was generated with AI assistance and reviewed for accuracy. Examples are provided to illustrate common scenarios and may not cover every case. Always test fixes in a development environment before applying to production. Spotted an error? Suggest a correction →