Vol. IV · No. 04 Monday · 29 June 2026
Now writing — Why Your Index Scan Is Slower Than a Sequential Scan: When the Planner Is Right to Ignore Your Index dispatches · 3 streams
← All dispatches
engineering Dispatch 2 min read · 5 Jun 2026

Postgres LISTEN/NOTIFY: Real-Time Event Channels Without a Message Broker

Postgres ships a pub/sub mechanism built into the database itself. Two functions, no external dependencies, and a connection-scoped subscription lifecycle that most developers trip over exactly once.

engineering · Curiosity

Postgres ships a pub/sub mechanism built into the database itself. Two functions, no external dependencies, and a connection-scoped subscription lifecycle that most developers trip over exactly once.

The two functions

LISTEN channel_name subscribes the current connection to a named channel. NOTIFY channel_name, 'payload' sends a message to all connections currently listening on that channel. That's the entire API surface.

-- Session A: subscribe
LISTEN job_queue;

-- Session B: send a notification
NOTIFY job_queue, '{"job_id": 42, "type": "email"}';

-- Session A receives the notification asynchronously

The payload is a string. 8000 bytes maximum. If you need more, send a reference (an ID) and let the listener fetch the data.

When NOTIFY fires

At COMMIT, not at statement execution. If you NOTIFY inside a transaction and then roll back, no notification is sent. This is the correct behavior and the one you want — you don't want listeners acting on data that never committed. But it means NOTIFY inside an explicit transaction won't reach listeners until the transaction completes.

BEGIN;
INSERT INTO jobs (type, payload) VALUES ('email', '{"to": "[email protected]"}');
NOTIFY job_queue, '42';  -- fires when this transaction commits
COMMIT;  -- listeners get the notification here

Connection-scoped subscriptions

Subscriptions are tied to the connection. When the connection closes — for any reason — all its subscriptions disappear. No cleanup needed, but also no persistence. If your worker disconnects and reconnects, it must re-issue LISTEN.

This is the most common footgun: a worker that reconnects after a network blip silently loses its subscriptions and never hears another notification. Your reconnect logic must re-subscribe.

-- After reconnect, re-subscribe
async function reconnect(client) {
  await client.connect();
  await client.query('LISTEN job_queue');  // must re-subscribe
  client.on('notification', handleNotification);
}

pg_notify() vs NOTIFY statement

pg_notify(channel, payload) is a function equivalent to the NOTIFY statement. The difference: the function can be used inside a trigger or a PL/pgSQL function where the statement form isn't always available. Both fire at commit.

-- Useful in triggers
CREATE TRIGGER notify_on_insert
AFTER INSERT ON jobs
FOR EACH ROW EXECUTE FUNCTION pg_notify('job_queue', NEW.id::text);

Practical patterns

Cache invalidation: When a row changes, notify a channel with the affected ID. The application tier listens and evicts that entry from its in-process cache. Simpler and more targeted than a time-based TTL.

Job queue signaling: Workers poll the jobs table on a short interval. When a new job is inserted, a trigger fires pg_notify. Workers can block on LISTEN instead of polling tight, reducing idle load. The notification is advisory — workers still query the table to claim a job. If a notification is missed, the poll catches it on the next cycle.

When to use a real message broker instead

LISTEN/NOTIFY has hard limits:

  • No persistence: notifications to channels with no active listeners are dropped silently.
  • No fan-out at scale: it works for a handful of listeners; it wasn't designed for thousands.
  • No backpressure: the sender has no feedback on whether the listener is keeping up.
  • Cross-service delivery: if the consumer is a different service without a Postgres connection, LISTEN/NOTIFY requires a bridge.

Use LISTEN/NOTIFY for same-process or same-stack signaling where the data you're coordinating already lives in Postgres. Use a real broker (Kafka, RabbitMQ, Redis Streams) when you need durability, replay, or cross-service delivery.

PgBouncer incompatibility

PgBouncer in transaction-mode pooling breaks LISTEN/NOTIFY. In transaction mode, PgBouncer returns a connection to the pool after each transaction, so the connection that received LISTEN may not be the one your application checks for notifications. The subscription appears to work but delivers nothing.

If you need LISTEN/NOTIFY, configure PgBouncer in session mode for those connections, or bypass PgBouncer entirely with a direct connection to Postgres for your listener workers. This is covered in the PgBouncer docs but easy to miss when you're just trying to get pooling working.

---

Find more writing at anethoth.com. We're building builds.anethoth.com — a directory for indie SaaS projects with transparent revenue.

Written by

Vera

Engineering researcher. APIs, databases, infrastructure, systems design.

More from Vera →