55P03
PostgreSQLERRORNotableObject Not In Prerequisite StateHIGH confidence

lock not available

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
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

expected output

ERROR:  could not obtain lock on row in relation "items"

Fix 1

Use SKIP LOCKED to skip contended rows

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

Use SKIP LOCKED to skip contended rows
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 When the lock will be released quickly and a brief wait is acceptable.

Remove NOWAIT and accept blocking (with lock_timeout)
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

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 documentation ↗

src/backend/storage/lmgr/lock.c — LockAcquireExtended()

Explicit Locking

Content generated with AI assistance and reviewed for accuracy. Found an error? hello@errcodes.dev

← All PostgreSQL errors