# Lock waits

> Heavyweight locks whose owner and blocking chain can usually be identified from SQL.
---

`Lock` is the most directly actionable class: a backend asked the lock manager for a heavyweight lock and an incompatible holder has not released it. Unlike LWLocks, these waits usually have rows in `pg_locks` and a useful result from `pg_blocking_pids()`.

## Read the blocking graph, not the victim count {#how-to-read}

Twenty waiters can all be symptoms of one idle transaction. Start at the head of the graph, then decide whether the holder is doing useful work, abandoned, or participating in an expected DDL/deploy window.

- **Normal:** sub-second lock handoff during ordinary writes or planned DDL.
- **Watch:** any user-facing waiter persists beyond its latency objective, or at least 10% of active sessions wait on the same root blocker.
- **Urgent:** the root holder is `idle in transaction`, the chain keeps growing, critical DDL blocks traffic, or deadlocks begin to rise.

## Events to recognize {#top-events}

| Event | Usually means |
| --- | --- |
| [relation](relation/) | Table/index-level lock conflict, often DDL versus DML |
| [transactionid](transactionid/) | Waiting for another transaction's outcome, commonly row updates |
| [tuple](tuple/) | Competing tuple locks or a long row-lock queue |
| [extend](extend/) | Sessions serialize while extending one relation |
| [virtualxid](virtualxid/) | DDL waits for transactions that might still use an object |
| [advisory](advisory/) | Application-defined advisory-lock coordination |

## Blocker query {#blocker-query}

```sql
SELECT w.pid AS waiting_pid, b.pid AS blocking_pid,
       w.wait_event, now() - w.query_start AS waiting_for,
       now() - b.xact_start AS blocker_xact_age,
       b.state AS blocker_state,
       left(w.query, 100) AS waiting_query,
       left(b.query, 100) AS blocking_query
FROM pg_stat_activity AS w
CROSS JOIN LATERAL unnest(pg_blocking_pids(w.pid)) AS x(pid)
JOIN pg_stat_activity AS b ON b.pid = x.pid
ORDER BY waiting_for DESC;
```

## Common misreads {#misreads}

- A session listed as a blocker is not automatically safe to terminate; it may be the only writer preserving an invariant.
- `transactionid` does not mean transaction-ID exhaustion.
- `relation` does not identify the relation by itself; join `pg_locks.relation` to `pg_class`.
- Increasing `lock_timeout` changes how long victims wait; it does not remove the blocker.

---

Section pages:

- [Lock: advisory](/lock/advisory/): Waiting to acquire an advisory user lock
- [Lock: applytransaction](/lock/applytransaction/): Waiting to acquire a lock on a remote transaction being applied by a logical replication subscriber
- [Lock: extend](/lock/extend/): Waiting to extend a relation
- [Lock: frozenid](/lock/frozenid/): Waiting to update pg_database.datfrozenxid and pg_database.datminmxid
- [Lock: object](/lock/object/): Waiting to acquire a lock on a non-relation database object
- [Lock: page](/lock/page/): Waiting to acquire a lock on a page of a relation
- [Lock: relation](/lock/relation/): Waiting to acquire a lock on a relation
- [Lock: spectoken](/lock/spectoken/): Waiting to acquire a speculative insertion lock
- [Lock: transactionid](/lock/transactionid/): Waiting for a transaction to finish
- [Lock: tuple](/lock/tuple/): Waiting to acquire a lock on a tuple
- [Lock: userlock](/lock/userlock/): Waiting to acquire a user lock
- [Lock: virtualxid](/lock/virtualxid/): Waiting to acquire a virtual transaction ID lock
