A PostgreSQL transaction can remain open while a lock acquired during part of that transaction is released. If the lock was acquired after a savepoint and execution rolls back to that savepoint, PostgreSQL releases the lock immediately rather than retaining it until the outer transaction ends.

That behavior creates a lock-lifetime boundary inside a transaction. The common rule that transaction locks last until commit or rollback remains useful, but savepoints add a narrower scope for locks acquired after the marked point.

A savepoint creates a reversible transaction segment

SAVEPOINT records a position inside the current transaction. Commands after that position can later be discarded with ROLLBACK TO SAVEPOINT while commands before it remain part of the transaction.

A minimal sequence has two distinct regions:

BEGIN;
UPDATE accounts SET state = 'open' WHERE id = 1;

SAVEPOINT candidate;
UPDATE accounts SET state = 'held' WHERE id = 2;

ROLLBACK TO SAVEPOINT candidate;
COMMIT;

The first update remains eligible to commit. The second update is undone by the rollback to candidate. The outer transaction itself does not end.

PostgreSQL applies the same boundary to locks acquired after the savepoint. A table or row lock obtained in the reversible region is released when that region is rolled back. Locks acquired before the savepoint remain governed by the outer transaction.

This differs from a full ROLLBACK, which ends the transaction and discards its transactional effects as a whole.

Lock lifetime follows acquisition position

Consider an explicit table lock acquired after a savepoint:

BEGIN;
SAVEPOINT before_lock;

LOCK TABLE inventory IN ACCESS EXCLUSIVE MODE;

ROLLBACK TO SAVEPOINT before_lock;

While the ACCESS EXCLUSIVE lock is held, conflicting operations can wait. After the rollback to before_lock, that lock is released even though the session is still inside the outer transaction.

Move the acquisition before the savepoint and the result changes:

BEGIN;
LOCK TABLE inventory IN ACCESS EXCLUSIVE MODE;

SAVEPOINT after_lock;
ROLLBACK TO SAVEPOINT after_lock;

The rollback does not cross the lock acquisition, so the lock remains held. Its ordinary transaction lifetime continues.

The relevant question is therefore not only whether a lock belongs to a transaction. Its position relative to savepoint boundaries also determines whether a partial rollback can terminate ownership.

Partial rollback changes concurrency before commit

A savepoint rollback can alter what competing transactions are permitted to do before the outer transaction reaches a final outcome.

Suppose transaction A modifies one resource, establishes a savepoint, then obtains a conflicting lock on another resource. Transaction B waits for that later lock. If A rolls back to the savepoint, B can become eligible to proceed even though A remains open and may eventually commit its earlier work.

The observable sequence can be represented as:

A: outer transaction active
A: savepoint established
A: lock acquired
B: waits on A
A: rollback to savepoint
A: outer transaction still active
B: later lock no longer held by A

Scheduling after release still depends on PostgreSQL lock queues, conflicting holders, and concurrent requests. Releasing A’s lock does not guarantee that B runs immediately. It removes that particular ownership constraint.

This matters for diagnostics because transaction age alone cannot establish the lifetime of every lock that transaction once held. A transaction may have acquired and released locks through subtransaction rollback while retaining other locks from earlier work.

Error recovery can make the boundary operationally significant

Inside an explicit transaction, a statement error normally leaves the transaction in an aborted state until suitable rollback action occurs. A savepoint can provide a recovery boundary: code can establish the savepoint, attempt an operation, and roll back to the savepoint after an error rather than abandoning all earlier work.

That pattern also affects lock ownership. Locks acquired inside the failed segment are released when the segment is rolled back.

PL/pgSQL exception blocks use subtransaction machinery with related lock behavior. When control escapes an exception-protected block because of an error, locks acquired inside that block are released as the subtransaction is rolled back.

The consequence is broader than discarded row changes. Error handling can modify the concurrency state visible to other sessions while preserving the surrounding transaction.

Applications that infer lock ownership solely from entry into an outer transaction can therefore retain an inaccurate model after local error recovery.

Release is not equivalent to savepoint rollback

RELEASE SAVEPOINT and ROLLBACK TO SAVEPOINT both alter savepoint state, but they have opposite effects on the work after the savepoint.

ROLLBACK TO SAVEPOINT discards commands executed after the savepoint and starts a new subtransaction at the same level. The named savepoint remains available for another rollback. Savepoints created after it are destroyed.

RELEASE SAVEPOINT removes the named savepoint and merges surviving work into the surrounding transaction or savepoint. It does not discard the effects of commands executed after the savepoint.

A lock acquired after a savepoint is therefore not released merely because that savepoint is released. The lock remains part of the surviving transaction state and normally continues until a later rollback crosses its acquisition or the enclosing transaction ends.

This semantic difference makes RELEASE unsuitable as a substitute for partial rollback when the intended effect includes undoing later lock acquisition.

Nested savepoints form nested rollback boundaries

A transaction can contain multiple savepoints:

outer transaction
  |
  +-- savepoint A
        |
        +-- lock X
        |
        +-- savepoint B
              |
              +-- lock Y

Rolling back to B crosses the acquisition of Y but not X, so Y can be released while X remains. Rolling back to A crosses both acquisitions and can release both.

PostgreSQL also permits repeated savepoint names. The most recently established unreleased savepoint with that name is the accessible one. Releasing it can expose an older savepoint with the same name again.

For lock-lifetime analysis, savepoint names are therefore less informative than the actual nesting and acquisition order. Two statements using the same textual name can refer to different rollback positions at different moments.

A trace that records only savepoint names without nesting or sequence information can obscure which locks a rollback actually crosses.

Cursor state marks a different semantic boundary

Not every operation after a savepoint behaves like transactional data changes or lock acquisition. PostgreSQL documents cursor behavior that is partly non-transactional with respect to savepoint rollback.

A cursor opened inside a savepoint is closed when that savepoint is rolled back. In contrast, if a cursor opened earlier is moved with FETCH or MOVE after the savepoint, rolling back does not restore its previous cursor position. Closing a cursor is also not undone.

This distinction prevents a broad assumption that ROLLBACK TO SAVEPOINT restores every session-visible detail to a byte-for-byte prior state.

Savepoints define transactional rollback semantics, with documented exceptions and subsystem-specific behavior. Lock release after a savepoint rollback is an explicit PostgreSQL rule, not evidence that all session state follows the same reversal model.

Advisory locks require separate scope analysis

PostgreSQL advisory locks add another boundary because their lifetime depends on the advisory-lock API used.

Transaction-level advisory locks participate in transaction semantics. Session-level advisory locks belong to the database session instead. A session-level advisory lock can survive transaction rollback and must be released explicitly or by ending the session.

That means a savepoint rollback must not be treated as a universal lock cleanup mechanism across every PostgreSQL lock facility.

The acquisition API remains decisive. Ordinary transaction locks acquired after a savepoint are released when rolling back to that savepoint. Session-scoped advisory locks intentionally use a different lifetime contract.

This separation is especially relevant with connection pools, where a physical database session can outlive many application transactions.

Lock observability is a snapshot of current ownership

pg_locks exposes locks currently held or awaited by PostgreSQL backends. A lock that disappears after ROLLBACK TO SAVEPOINT reflects a real ownership transition, not the end of the outer transaction.

Correlating pg_locks with pg_stat_activity can therefore show a backend that remains active in the same transaction while a previously visible lock is no longer present.

Diagnostics based only on transaction start time can miss this internal history. Conversely, application logs that record a successful lock acquisition do not prove that the lock is still held if a later savepoint rollback crossed that acquisition.

Accurate concurrency analysis needs the current server state plus the transaction control path that produced it.

Savepoints make lock ownership locally reversible

Savepoints divide one PostgreSQL transaction into regions with different rollback reach. Locks acquired before a savepoint can survive a rollback to it; locks acquired after it are released when that later region is discarded.

The outer transaction boundary still governs work that remains. Savepoint rollback does not commit earlier changes, start an unrelated transaction, or erase every form of session state.

Its narrower effect is enough to change concurrency materially: ownership created inside a reversible transaction segment can end before the transaction itself ends. Lock lifetime is therefore determined by both transaction scope and acquisition position within the savepoint structure.