PostgreSQL Asynchronous Messaging with LISTEN and NOTIFY
PostgreSQL includes a built-in publish-subscribe framework that enables database sessions to exchange asynchronous signals without relying on external message brokers. This communication layer is implemented through the LISTEN and NOTIFY commands, allowing backend processes and client applications to coordinate over named channels.
Basic Subscription and Publication
To establish a communication path, a client connection must first register as a subscriber. In one terminal, attach a listener to a specific channel:
app_db=# LISTEN event_stream;
LISTEN
From a separate database connection, disptach a notification to that channel. An optional text payload can be appended to the signal:
app_db=# NOTIFY event_stream, 'service_ready';
NOTIFY
The publishing statement executes immediately. However, the listening session only processes incoming payloads when it runs a subsequent query. Executing any SQL statement in the first session triggers message delivery:
app_db=# SELECT current_timestamp;
current_timestamp
----------------------------
2023-10-25 14:32:10.123456+00
(1 row)
Asynchronous notification "event_stream" with payload "service_ready" received from server process with PID 7821.
Core Commands and Utility Functions
The notification subsystem operates through a small set of declarative statements and server-side functions:
LISTEN channel: Registers the active connection as a subscriber. Executing this command multiple times for the same channel is idempotent and generates no errors.UNLISTEN { channel | * }: Removes an active subscription. Using the wildcard*clears all channel registrations for the current session.NOTIFY channel [, payload]: Broadcasts a signal to all listeners of the target channel. The payload string is optional and defaults to empty.pg_notify(channel text, payload text): A function equivalent to theNOTIFYcommand, useful for embedding within procedural code or dynamic SQL.pg_listening_channels(): Returns a set containing all channel names currently monitored by the active session.
Querying active subscriptions is straightforward:
app_db=# LISTEN db_metrics;
LISTEN
app_db=# LISTEN cache_invalidation;
LISTEN
app_db=# SELECT * FROM pg_listening_channels();
pg_listening_channels
-----------------------------
db_metrics
cache_invalidation
(2 rows)
Multi-Client Delivery and Transactional Boundaries
The framework natively supports broadcasting. When a signal is dispatched, every active subscriber to the target channel receives the payload independently.
A critical design characteristic of NOTIFY is its strict transactional semantics. Notifications generated inside an uncommitted transaction remain queued in memory and are not delivered until the transaction successfully commits. If the transaction is rolled back, the pending signals are permanently discarded.
app_db=# BEGIN;
app_db=# NOTIFY event_stream, 'queued_action';
app_db=# ROLLBACK;
-- Checking the listener session after rollback yields no new messages.
Only after replacing ROLLBACK with COMMIT will the payload propagate to the registered listeners.
Ordering Guarantees and Deduplication
The database engine enforces strict delivery sequencing. Signals originating within a single transaction are delivered in the exact order they were queued. Across separate transactions, delivery follows the chronological sequence of transaction commits.
To minimize overhead, PostgreSQL automatically deduplicates identical notifications within a transaction scope. If the same channel and payload combination is emitted multiple times before commit, subscribers receive only a single instance.
app_db=# BEGIN;
app_db=# NOTIFY event_stream, 'batch_processed';
app_db=# NOTIFY event_stream, 'batch_processed';
app_db=# COMMIT;
-- Polling the listener will show exactly one "batch_processed" payload.
System Limits and Protocol Constraints
An internal queue stores pending notifications awaiting consumption by active listeners. The default maximum capacity is 8 GB. Queue exhautsion typically occurs when a session subscribes to a channel, opens a long-lived transaction, and fails to periodically process incoming messages. Once the queue reaches its limit, committing a transaction that contains new NOTIFY calls will fail.
Furthermore, asynchronous notifications cannot participate in two-phase commit protocols. Attempting to prepare a transaction that includes LISTEN, UNLISTEN, or NOTIFY statements will immediately abort:
app_db=# BEGIN;
app_db=# NOTIFY event_stream, 'phase_one_complete';
app_db=# PREPARE TRANSACTION 'xid_global_99';
ERROR: NOTIFY cannot PREPARE a transaction that has executed LISTEN, UNLISTEN, or NOTIFY