LISTEN Up! Asynchronous Notifications with LISTEN/NOTIFY in YugabyteDB

YugabyteDB v2025.2.3 introduced Early Access support for PostgreSQL-compatible LISTEN, NOTIFY, and UNLISTEN in YSQL.

That means applications can use the familiar PostgreSQL pub/sub-style notification pattern to send lightweight asynchronous signals between database sessions.

This is not meant to replace Kafka, CDC, a durable queue, or a job processing system.

But for lightweight coordination, cache invalidation, UI refresh nudges, background worker wake-ups, or “something changed, go check the table” signals, LISTEN / NOTIFY can be very handy.
💡 The key idea: Use NOTIFY to send a small signal, usually with a small payload, and let listeners react by querying the real source of truth from tables.

What LISTEN/NOTIFY Does

YugabyteDB supports the same basic LISTEN, NOTIFY, and UNLISTEN syntax that PostgreSQL users are familiar with.

A session can listen on a named channel, another session can send a notification on that channel, and the listener receives the notification asynchronously.

Command What it does
LISTEN channel_name; Registers the current session as a listener on a named channel.
NOTIFY channel_name; Sends a notification on a channel with no payload.
NOTIFY channel_name, 'payload'; Sends a notification on a channel with a small text payload.
SELECT pg_notify(...); Function form of NOTIFY, useful when building dynamic channel names or payloads.
UNLISTEN channel_name; Removes the current session from a specific channel.
UNLISTEN *; Removes all listener registrations for the current session.

The notification itself should usually be treated as a lightweight signal. For example, send the event type and a primary key in the payload, then have the listener query the table for the full row.

💡 Tip: Use NOTIFY to say “something changed,” not to carry the entire business event. Store the durable data in a table and let the listener query it.

Enable LISTEN/NOTIFY

LISTEN / NOTIFY is currently an Early Access feature in YugabyteDB and is disabled by default.

To enable it, set the following flag on both YB-TServers and YB-Masters:

				
					--ysql_yb_enable_listen_notify=true
				
			

For a local yugabyted demo cluster, pass the flag through both --tserver_flags and --master_flags.

				
					./bin/yugabyted start \
  --advertise_address=127.0.0.1 \
  --base_dir=$HOME/yb-listen-notify-demo/node1 \
  --tserver_flags="ysql_yb_enable_listen_notify=true" \
  --master_flags="ysql_yb_enable_listen_notify=true"
				
			

If you are using YugabyteDB Anywhere, add the flag to both the Master and TServer gFlag sections for the universe.

⚠️ Startup note: After enabling the feature, the leader Master creates internal objects in the background, including the yb_system database and the yb_system.pg_yb_notifications table. If you try LISTEN or NOTIFY immediately after startup and get an error, wait a few seconds and try again.

You can verify that the cluster is on a supported version with:

				
					SELECT split_part(version(), '-', 3) AS yb_version;
				
			

Example output:

				
					.yb_version
------------
 2025.2.4.0
(1 row)
				
			

For our demo, I am using YugabyteDB 2025.2.4.0, which includes Early Access support for LISTEN, NOTIFY, and UNLISTEN.

💡 Tip: Enable the flag on both Masters and TServers. If it is only enabled on one process type, the feature will not be fully available.

Demo 1: The Simplest Possible LISTEN/NOTIFY

For the first demo, open two separate ysqlsh sessions:

  • ● The first session will listen on a channel.
  • ● The second session will send a notification to that channel.
Terminal 1: Start Listening

In the first ysqlsh session, run:

				
					LISTEN ybtip_events;
				
			

Check which channels the current session is listening on:

				
					SELECT * FROM pg_listening_channels();
				
			

Example output:

				
					.pg_listening_channels
-----------------------
 ybtip_events
(1 row)
				
			

At this point, Terminal 1 is registered as a listener on the ybtip_events channel.

Terminal 2: Send a Notification

In the second ysqlsh session, run:

				
					NOTIFY ybtip_events;
				
			

The command completes immediately:

				
					NOTIFY
				
			
Terminal 1: Receive the Notification

In ysqlsh, asynchronous notifications are displayed when the client checks for them. Running another simple statement is usually enough to make the notification appear.

Back in Terminal 1, run:

				
					SELECT 'polling for notifications' AS status;
				
			

Example output:

				
					.         status
---------------------------
 polling for notifications
(1 row)

Asynchronous notification "ybtip_events" received from server process with PID 2300889.
				
			
💡 What just happened? Terminal 1 registered interest in the ybtip_events channel. Terminal 2 sent a notification on that channel. When Terminal 1 checked for messages, YugabyteDB delivered the asynchronous notification to the listening session.

Demo 2: Send a Payload

A notification can also include a small text payload.

This is useful when you want to send a little bit of context with the signal, such as an event type, an ID, or a small JSON document.

Terminal 2: Send a Notification with a Payload

In Terminal 2, run:

				
					NOTIFY ybtip_events, 'Hello from YugabyteDB!';
				
			

Example output:

				
					NOTIFY
				
			
Terminal 1: Receive the Payload

Back in Terminal 1, run another simple statement so ysqlsh checks for asynchronous notifications:

				
					SELECT 'polling again' AS status;
				
			

Example output:

				
					.   status
---------------
 polling again
(1 row)

Asynchronous notification "ybtip_events" with payload "Hello from YugabyteDB!" received from server process with PID 2300889.
				
			

The listener received both the channel name and the payload.

💡 Payload tip: Keep the payload small. For most application patterns, send just enough information for the listener to know what changed, such as an event name and primary key. Then let the listener query the real data from a table.

Demo 3: Use pg_notify() for Dynamic Payloads

The pg_notify() function is the function form of NOTIFY.

It is useful when you want to build the channel name or payload dynamically.

For example, instead of hardcoding the channel and payload, you can create them from expressions.

Terminal 2: Send a Dynamic Notification

In Terminal 2, run:

				
					SELECT pg_notify(
         'ybtip_' || 'events',
         json_build_object(
           'event', 'demo',
           'message', 'payload built with pg_notify()',
           'created_at', clock_timestamp()
         )::text
       );
				
			

Example output:

				
					.pg_notify
-----------

(1 row)
				
			
Terminal 1: Receive the JSON Payload

Back in Terminal 1, run another simple statement so ysqlsh checks for asynchronous notifications:

				
					SELECT 'checking for JSON payload' AS status;
				
			

Example output:.

				
					.         status
---------------------------
 checking for JSON payload
(1 row)

Asynchronous notification "ybtip_events" with payload "{"event" : "demo", "message" : "payload built with pg_notify()", "created_at" : "2026-06-27T00:43:35.824961+00:00"}" received from server process with PID 2300889.
				
			

This gives you a little more flexibility than plain NOTIFY, especially when the payload is built from table data, trigger logic, or application context.

💡 Practical pattern: Use pg_notify() when the channel name or payload needs to be built dynamically. For example, a trigger can send a JSON payload containing the operation type, table name, and primary key of the changed row.

Demo 4: A More Realistic Table + Trigger Example

Now let’s make the demo a little more realistic.

In this example, we’ll create a small order-status table. Then we’ll add a trigger that sends a notification whenever an order is inserted or the order status changes.

This is a common pattern:

  • ● Store the durable data in a table.
  • ● Use NOTIFY to wake up listeners and tell them something changed.
Terminal 2: Create the Demo Table

In Terminal 2, run:

				
					DROP TABLE IF EXISTS ybtip_order_status CASCADE;

CREATE TABLE ybtip_order_status (
  order_id     BIGSERIAL PRIMARY KEY,
  customer_id  TEXT NOT NULL,
  status       TEXT NOT NULL,
  updated_at   TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
				
			

Create a trigger function that builds a small JSON payload and sends it with pg_notify():

				
					CREATE OR REPLACE FUNCTION ybtip_notify_order_status()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
  v_payload text;
BEGIN
  v_payload := json_build_object(
    'operation', TG_OP,
    'order_id', NEW.order_id,
    'customer_id', NEW.customer_id,
    'status', NEW.status,
    'updated_at', NEW.updated_at
  )::text;

  PERFORM pg_notify('ybtip_order_status', v_payload);

  RETURN NEW;
END;
$$;
				
			

Create the trigger:

				
					CREATE TRIGGER trg_ybtip_order_status_notify
AFTER INSERT OR UPDATE OF status
ON ybtip_order_status
FOR EACH ROW
EXECUTE FUNCTION ybtip_notify_order_status();
				
			
Terminal 1: Listen for Order Status Events

In Terminal 1, run:

				
					LISTEN ybtip_order_status;
				
			

Check that the session is listening:

				
					SELECT * FROM pg_listening_channels();
				
			

Expected output:

				
					.pg_listening_channels
-----------------------
 ybtip_events
 ybtip_order_status
(2 rows)
				
			
Terminal 2: Insert an Order

In Terminal 2, run:

				
					INSERT INTO ybtip_order_status (customer_id, status)
VALUES ('CUST-101', 'created')
RETURNING *;
				
			

Expected output:

				
					.order_id | customer_id | status  |          updated_at
----------+-------------+---------+-------------------------------
        1 | CUST-101    | created | 2026-06-27 00:53:41.100657+00
(1 row)

INSERT 0 1
				
			
Terminal 1: Receive the Insert Notification

Back in Terminal 1, run:

				
					SELECT 'order listener is awake' AS status;
				
			

Expected output:

				
					.        status
-------------------------
 order listener is awake
(1 row)

Asynchronous notification "ybtip_order_status" with payload "{"operation" : "INSERT", "order_id" : 1, "customer_id" : "CUST-101", "status" : "created", "updated_at" : "2026-06-27T00:53:41.100657+00:00"}" received from server process with PID 2300889.
				
			
Terminal 2: Update the Order Status

In Terminal 2, run:

				
					UPDATE ybtip_order_status
SET status = 'shipped',
    updated_at = clock_timestamp()
WHERE order_id = 1
RETURNING *;
				
			

Expected output:

				
					 order_id | customer_id | status  |          updated_at
----------+-------------+---------+-------------------------------
        1 | CUST-101    | shipped | 2026-06-27 00:57:04.357722+00
(1 row)

UPDATE 1
				
			
Terminal 1: Receive the Update Notification

Back in Terminal 1, run:

				
					SELECT 'checking for update event' AS status;
				
			

Example output:

				
					.         status
---------------------------
 checking for update event
(1 row)

Asynchronous notification "ybtip_order_status" with payload "{"operation" : "UPDATE", "order_id" : 1, "customer_id" : "CUST-101", "status" : "shipped", "updated_at" : "2026-06-27T01:00:43.387713+00:00"}" received from server process with PID 2300889.
				
			
💡 Why this pattern works well: The row change is stored durably in the table, while NOTIFY acts as a lightweight wake-up signal. If the listener needs more detail, it can query ybtip_order_status by order_id.

Demo 5: Transaction Semantics

LISTEN and NOTIFY are transaction-aware:

  • ● A notification is not delivered until the transaction commits.
  • ● If the transaction rolls back, the notification is discarded.
  • ● This is useful because listeners only hear about changes that actually committed.
Rollback Means No Notification

In Terminal 2, run:

				
					BEGIN;

NOTIFY ybtip_events, 'you should not see this';

ROLLBACK;
				
			

Example output:

				
					BEGIN
NOTIFY
ROLLBACK
				
			

Back in Terminal 1, check for notifications:

				
					SELECT 'checking after rollback' AS status;
				
			

Example output:

				
					.        status
-------------------------
 checking after rollback
(1 row)
				
			

No asynchronous notification should appear because the transaction was rolled back.

Commit Makes the Notification Visible

Now, in Terminal 2, run:

				
					BEGIN;

NOTIFY ybtip_events, 'you should see this after commit';

COMMIT;
				
			

Example output:

				
					BEGIN
NOTIFY
COMMIT
				
			

Back in Terminal 1, check again:

				
					SELECT 'checking after commit' AS status;
				
			

Example output:

				
					Asynchronous notification "ybtip_events" with payload "you should see this after commit" received from server process with PID 2300889.

         status
-------------------------
 checking after commit
(1 row)
				
			

This time the notification is delivered because the transaction committed.

💡 Why this matters: A listener should not react to data changes that never committed. With NOTIFY, the signal follows transaction commit behavior, so listeners only receive notifications from successful transactions.

Demo 6: Duplicate Notifications Are Coalesced

If the same transaction sends multiple notifications with the same channel and the same payload, YugabyteDB coalesces them into a single notification.

This matches PostgreSQL behavior.

The key detail is that both the channel and payload must be the same.

Terminal 2: Send Duplicate Notifications in One Transaction

In Terminal 2, run:

				
					BEGIN;

NOTIFY ybtip_events, 'same payload';
NOTIFY ybtip_events, 'same payload';
NOTIFY ybtip_events, 'same payload';

NOTIFY ybtip_events, 'different payload';

COMMIT;
				
			

Example output:

				
					BEGIN
NOTIFY
NOTIFY
NOTIFY
NOTIFY
COMMIT
				
			
Terminal 1: Check What Was Delivered

Back in Terminal 1, run:

				
					SELECT 'checking coalescing behavior' AS status;
				
			

Example output:

				
					.           status
------------------------------
 checking coalescing behavior
(1 row)

Asynchronous notification "ybtip_events" with payload "same payload" received from server process with PID 2300889.
Asynchronous notification "ybtip_events" with payload "different payload" received from server process with PID 2300889.
				
			

Even though same payload was sent three times, it was delivered once because the channel and payload were identical inside the same transaction.

The different payload notification was delivered separately because the payload was different.

💡 Coalescing behavior: Duplicate notifications are coalesced only when both the channel and payload match within the same transaction. This helps avoid unnecessary duplicate wake-up signals from a single transaction.

Demo 7: A Listener Inside a Transaction Holds Notifications

There is one more transaction behavior that is important to understand.

If a listening session is inside a transaction, incoming notifications are held until that transaction completes.

In other words, the listener can be registered for a channel, but if that listener is sitting inside an open transaction, notifications are not delivered to that session until the transaction ends.

Terminal 1: Start a Transaction While Listening

In Terminal 1, make sure you are still listening on ybtip_events:

				
					LISTEN ybtip_events;
				
			

Expected output:

				
					LISTEN
				
			

Now start a transaction:

				
					BEGIN;

SELECT 'listener is now inside a transaction' AS status;
				
			

Example output:

				
					BEGIN

                status
--------------------------------------
 listener is now inside a transaction
(1 row)
				
			

Leave Terminal 1 inside the open transaction.

Terminal 2: Send a Notification

In Terminal 2, run:

				
					NOTIFY ybtip_events, 'held until listener transaction completes';
				
			

Example output:

				
					NOTIFY
				
			
Terminal 1: Check While Still Inside the Transaction

Back in Terminal 1, run:

				
					SELECT 'still inside transaction' AS status;
				
			

Example output:

				
					.         status
--------------------------
 still inside transaction
(1 row)
				
			

The notification should not be delivered yet because the listening session is still inside a transaction.

Terminal 1: Commit the Listener Transaction

Now commit the transaction in Terminal 1:

				
					COMMIT;
				
			

Expected output:

				
					COMMIT
Asynchronous notification "ybtip_events" with payload "held until listener transaction completes" received from server process with PID 2300889.
				
			

The notification is delivered as soon as the listener’s transaction completes.

💡 Listener behavior: A listening session receives notifications between transactions. If the listener is inside a transaction, incoming notifications are held until that transaction finishes. For listener-style application connections, avoid leaving long-running transactions open.

Demo 8: Stop Listening with UNLISTEN

When a session no longer needs to receive notifications, use UNLISTEN.

You can stop listening on a specific channel, or stop listening on all channels.

Terminal 1: Check Current Listening Channels

In Terminal 1, run:

				
					SELECT * FROM pg_listening_channels();
				
			

Example output:

				
					.pg_listening_channels
-----------------------
 ybtip_events
 ybtip_order_status
(2 rows)
				
			
Terminal 1: Stop Listening on One Channel

To stop listening on only the ybtip_events channel, run:

				
					UNLISTEN ybtip_events;
				
			

Example output:

				
					UNLISTEN
				
			

Check the current listening channels again:

				
					SELECT * FROM pg_listening_channels();
				
			

Expected output:

				
					.pg_listening_channels
-----------------------
 ybtip_order_status
(1 row)
				
			

The session is no longer listening on ybtip_events, but it is still listening on ybtip_order_status.

Terminal 1: Stop Listening on All Channels

To remove all listener registrations for the current session, run:

				
					UNLISTEN *;
				
			

Expected output:

				
					UNLISTEN
				
			

Confirm that the session is no longer listening on any channels:

				
					SELECT * FROM pg_listening_channels();
				
			

Expected outout:

				
					.pg_listening_channels
-----------------------
(0 rows)
				
			
💡 Session scope: LISTEN registrations belong to the current database session. When the session ends, its listener registrations go away automatically. Use UNLISTEN when you want to stop listening before the session closes.

Final Takeaway

LISTEN / NOTIFY gives YugabyteDB users a familiar PostgreSQL-compatible way to send lightweight asynchronous signals between YSQL sessions.
In this tip, we walked through the basics:
  • ● Registering a session with LISTEN
  • ● Sending notifications with NOTIFY
  • ● Including a small payload
  • ● Using pg_notify() for dynamic messages
  • ● Sending notifications from a trigger
  • ● Understanding commit, rollback, and duplicate-notification behavior
  • ● Stopping listeners with UNLISTEN

The most important design pattern is to treat a notification as a signal, not as the durable source of truth.

Store the real business data in a table, commit the transaction, and use `NOTIFY` to wake up listeners so they can query the committed data.

💡 Final tip: Use LISTEN / NOTIFY for lightweight “something changed” signals. If the data matters, write it to a table first and let the listener query it after commit.
LISTEN / NOTIFY is not a replacement for Kafka, CDC, RabbitMQ, or a durable job queue.

But for simple coordination, cache invalidation, UI refresh signals, or background worker wake-ups, it can be a clean and useful tool in the YugabyteDB toolbox.

Have Fun!

First visit to the new YugabyteDB office in Sunnyvale, so I had to capture the entryway. New space, fresh energy, and the same mission to build the future of distributed SQL... and Meko ... and AMP!