A PostgreSQL advisory lock can survive a transaction rollback when it was acquired at session scope. The SQL transaction may have ended with no committed data changes, yet the same database session can continue holding the application-defined lock until an explicit release or session termination.

That behavior places lock lifetime at an interface boundary that is easy to blur in pooled applications. Advisory locks have application-defined meaning, but PostgreSQL still gives each acquisition precise server-side scope.

Two APIs create different lifetime boundaries

PostgreSQL exposes session-level functions such as pg_advisory_lock and transaction-level functions such as pg_advisory_xact_lock. Both coordinate on application-defined lock identifiers, and conflicting requests for the same identifier can block each other. Their release semantics differ.

A session-level lock remains associated with the database session until it is explicitly released or the session ends. A transaction-level lock is released automatically when the current transaction ends, whether by commit or rollback. PostgreSQL provides no explicit unlock operation for a transaction-level advisory lock.

The distinction is therefore not merely naming. It determines which server event terminates ownership.

pg_advisory_lock       -> session lifetime
pg_advisory_xact_lock  -> transaction lifetime

Application code that treats both forms as equivalent mutex calls can assign the wrong lifetime to a critical section.

Rollback does not reverse a session-level acquisition

Database rollback reverses transactional effects according to PostgreSQL transaction semantics. A session-level advisory lock is deliberately outside that lifetime rule.

If a transaction calls pg_advisory_lock(42), later encounters an error, and rolls back, the session can still own advisory lock 42. A competing session requesting a conflicting lock on the same identifier can remain blocked after the first transaction has ended.

This is materially different from row and table locks acquired as part of ordinary transactional work, which are normally released at transaction end. It is also different from pg_advisory_xact_lock(42), whose ownership terminates with the transaction.

The observable state can therefore contain no open transaction from the original operation while still containing its session-level advisory lock.

Connection pools extend the meaning of session scope

A database connection pool usually preserves physical database sessions across many logical application operations. Returning a connection to the pool does not end its PostgreSQL session.

That distinction changes the failure boundary for session-level advisory locks. If application code acquires one and returns the connection without the matching release, the lock can remain owned while the pool assigns that same session to unrelated work.

The lock is not transferred to another PostgreSQL session. Ownership remains with the original backend. What changes is the application-level association: a later request can inherit a pooled connection that already carries session state created by earlier work.

This is a general property of session-scoped database features. Pool checkout boundaries and server session boundaries are not identical. Advisory locks make the mismatch visible because retained ownership can block competing sessions.

Transaction-level advisory locks align more closely with transaction-scoped application work because commit or rollback supplies the release boundary. That alignment is conditional on the critical section fitting entirely inside one transaction.

Repeated session acquisitions stack

Session-level advisory lock acquisition is reentrant for the owning session. If the same session successfully acquires the same lock identifier multiple times, PostgreSQL counts those acquisitions. Matching unlock operations are required before another session can obtain a conflicting lock, unless the owning session ends.

This creates a second lifetime hazard beyond a completely missing unlock. Code can release a lock once and still retain it because an earlier path acquired the same identifier more than once.

A successful pg_advisory_unlock therefore establishes that one matching acquisition was released. It does not, by itself, prove that the session has no remaining hold on that identifier.

Transaction-level requests use transaction lifetime instead. Their release is tied to transaction end rather than a manually balanced sequence of unlock calls.

Lock keys are coordination names, not protected objects

PostgreSQL does not attach application semantics to an advisory lock key. A key can represent a customer, migration, scheduled job, file, aggregate, or any other resource chosen by the application. The database coordinates lock requests that use matching identifiers and compatible lock modes; it does not verify that every code path touching the represented resource participates.

An advisory lock can therefore provide mutual exclusion only among participants that follow the same key mapping and locking protocol.

This separates advisory locking from constraints enforced directly by database data structures. A unique constraint, for example, is checked by PostgreSQL when relevant writes occur. An advisory lock has no comparable automatic relationship to a row merely because application code derived its key from that row’s identifier.

Key construction is consequently part of the concurrency protocol. Two operations intended to conflict must map to the same advisory key space. Operations that should proceed independently should not collide through an accidental mapping.

Blocking and nonblocking calls expose different control flow

The standard advisory lock functions can wait for a conflicting owner to release the requested key. PostgreSQL also provides pg_try_advisory_lock and transaction-scoped counterparts that return immediately with a boolean result when the lock cannot be acquired at once.

This choice changes application control flow, not lock identity or lifetime. A successful session-level try call still creates a session-level hold. A successful transaction-level try call still ends with the transaction.

Timeout behavior is another separate layer. A caller that does not want indefinite waiting can rely on nonblocking acquisition or applicable database timeout controls, but cancellation of a waiting request is distinct from release of a lock already acquired.

Conflating those states can produce misleading diagnostics: a request waiting for ownership, a request that failed a try call, and a session already holding the key represent different concurrency states.

Advisory locks share server lock resources

Advisory locks are represented in PostgreSQL’s lock manager and are visible through pg_locks. They share the server’s lock-memory pool with regular locks, so advisory locking is not an unbounded external namespace.

This matters for designs that create very large numbers of simultaneously held advisory keys. The semantic model may make every key valid, while server resource limits still bound the number of lock entries that can exist at once.

The relevant capacity is configured and workload-dependent. A design should not infer a universal safe count from the width of the advisory key type.

pg_locks also provides an operational boundary between application intent and actual server state. When a coordination path appears stuck, lock rows can establish which backend holds or awaits an advisory lock without relying solely on application logs.

Scope is part of the concurrency contract

Session and transaction advisory locks can coordinate on the same application-defined resource, but they encode different ownership lifetimes. Session scope follows the backend connection and survives rollback. Transaction scope follows the transaction and ends automatically with commit or rollback.

That difference becomes especially consequential when connection pooling separates logical requests from physical sessions. A lock intended to protect one transaction can outlive that work if acquired through the session-level API, while a lock intended to span several transactions cannot be represented by a transaction-level acquisition.

The acquisition function therefore specifies more than a locking operation. It selects the event that releases ownership, and that event becomes part of the application’s concurrency contract.