# 55P03 — lock_not_available

> Source-backed full entry for PostgreSQL SQLSTATE 55P03.
---

# 55P03

## At a glance {#at-a-glance}

`55P03` reports that a requested lock could not be obtained under the operation's selected wait policy. Fixed paths cover row locks, relation locks, `LOCK TABLE`, `lock_timeout`, and maintenance commands that skip work.

<!-- BEGIN SQLSTATE FACTS: generated by scripts/generate.py; do not edit -->

| Field | Value |
| --- | --- |
| SQLSTATE | `55P03` |
| Condition | `lock_not_available` |
| Status | `active` |
| Known present by | `8.0.0` |
| Locked snapshots | `9.0.23, 9.1.24, 9.2.24, 9.3.25, 9.4.26, 9.5.25, 9.6.24, 10.23, 11.22, 12.22, 13.23, 14.24, 15.19, 16.15, 17.11, 18.6, 19beta3` |
| Macros | `ERRCODE_LOCK_NOT_AVAILABLE` |
| Aliases | `—` |

<!-- source facts: data/errcodes/55P03.json -->
<!-- END SQLSTATE FACTS -->

## Meaning {#meaning}

Row-lock callers use `LockWaitError` and `ConditionalXactLockTableWait()`/`ConditionalMultiXactIdWait()` to fail immediately with `could not obtain lock on row in relation "%s"`. Relation and `LOCK TABLE` callers use `ConditionalLockRelationOid()` and report a relation variant. A `lock_timeout` interrupt is another ERROR path, `canceling statement due to lock timeout`; it means the wait exceeded the configured timeout, not that `NOWAIT` was requested.

VACUUM and ANALYZE use the same SQLSTATE when they cannot acquire their relation lock, but their `ereport(elevel)` is deliberately non-ERROR: ordinary maintenance reports a WARNING, and an autovacuum verbose path can use LOG. The command skips that object and continues, so a warning is not an aborted transaction.

## Diagnosis {#diagnosis}

Keep the complete primary message, severity, relation or row name, lock mode, and statement. Inspect `pg_stat_activity`, `pg_locks`, blocker PIDs, transaction age, and whether the statement used `NOWAIT`, `lock_timeout`, or a maintenance command. Do not turn a skipped VACUUM/ANALYZE warning into a failed DML diagnosis.

The accepted row-lock case used a separate blocker and an autocommit contender. In an explicit transaction, a lock ERROR leaves the session `INERROR` until `ROLLBACK` or `ROLLBACK TO SAVEPOINT`; the accepted autocommit contender remained `IDLE` after its failed statement.

## Response {#response}

Coordinate or release the blocker, or choose a deliberate wait/timeout/skip policy. For an ERROR in an explicit transaction, roll back the whole transaction or to an existing savepoint before issuing more SQL; verify which work was committed before replaying. In autocommit, change the blocking or timeout condition first, then retry the statement with an idempotency rule. For a maintenance WARNING/LOG, record the skipped relation and run maintenance later; do not issue `ROLLBACK` for a command that did not abort the transaction.

## Observed diagnostics {#observed-diagnostics}

The selected registry created a blocker holding `SELECT ... FOR UPDATE` in an explicit transaction and an autocommit contender issuing `FOR UPDATE NOWAIT`. PostgreSQL 18.6 and 10.21 returned `ERROR` / `55P03` with `could not obtain lock on row in relation "nowait_rows"`; the contender stayed `IDLE`. After the blocker rolled back, the same contender updated `marker` to `repaired` and verified it.

## Representative case {#case}

The SQL is a role-labelled registry excerpt. Setup is run once; blocker and contender must be separate sessions. It covers the row-lock `NOWAIT` branch only, not `lock_timeout` or the maintenance skip branches.

<!-- BEGIN SQLSTATE SNIPPET: nowait_row_lock -->
```sql
-- setup (one maintenance session)
CREATE TABLE nowait_rows(id integer PRIMARY KEY, marker text NOT NULL);
INSERT INTO nowait_rows VALUES (1, 'seed');

-- blocker session: keep this transaction open
BEGIN;
SELECT id FROM nowait_rows WHERE id = 1 FOR UPDATE;

-- contender session, autocommit on: this is the 55P03 operation
SELECT id FROM nowait_rows WHERE id = 1 FOR UPDATE NOWAIT;

-- blocker session
ROLLBACK;

-- contender session after the blocker is released
UPDATE nowait_rows SET marker = 'repaired' WHERE id = 1 RETURNING marker;
SELECT marker FROM nowait_rows WHERE id = 1;
```
<!-- END SQLSTATE SNIPPET -->

The passed case confirms this row-lock mechanism, its `ERROR`/`IDLE` boundary, and its repair step only; it does not prove that every lock mode or maintenance command has the same severity.

## Versions {#versions}

The locked catalogue records this condition from 8.0.0; fixed source coverage is PostgreSQL 18.6. The accepted runtime was observed on PostgreSQL 18.6 and 10.21.

## Related {#related}

[`55P02`](../55p02/), [`40001`](../40001/), [`57014`](../57014/)

## Sources {#sources}

[src/backend/access/heap/heapam.c#L5178-5210](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/access/heap/heapam.c#L5178)

[src/backend/catalog/namespace.c#L585-610](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/catalog/namespace.c#L585)

[src/backend/tcop/postgres.c#L3420-3444](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/tcop/postgres.c#L3420)

[src/backend/commands/lockcmds.c#L130-145](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/commands/lockcmds.c#L130)

[src/backend/commands/vacuum.c#L835-880](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/commands/vacuum.c#L835)

The structured [evidence record](../data/evidence/55p03.json) records fixed message roles, maintenance severity, and accepted runtime artifacts.
