PostgreSQL LISTEN/NOTIFY for pub-sub messaging

Maria Garcia Feb 2026
2 tabs
-- Publisher: Send notification
NOTIFY new_order, 'Order #12345 created';

-- Subscriber: Listen for notifications
LISTEN new_order;

-- In another connection, will receive notification
-- Client libraries handle notification delivery

-- Unlisten
UNLISTEN new_order;
UNLISTEN *;  -- Unlisten all channels

-- Send notification with JSON payload
SELECT pg_notify(
  'user_updates',
  json_build_object(
    'user_id', 123,
    'action', 'updated',
    'timestamp', NOW()
  )::TEXT
);

-- Trigger-based notifications
CREATE OR REPLACE FUNCTION notify_new_order()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify(
    'new_order',
    json_build_object(
      'order_id', NEW.id,
      'user_id', NEW.user_id,
      'total', NEW.total,
      'created_at', NEW.created_at
    )::TEXT
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER order_notification
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION notify_new_order();

-- Now every insert sends notification
INSERT INTO orders (user_id, total) VALUES (123, 99.99);
-- Listeners receive: {"order_id": 456, "user_id": 123, ...}

-- Cache invalidation pattern
CREATE OR REPLACE FUNCTION notify_cache_invalidation()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify(
    'cache_invalidate',
    json_build_object(
      'table', TG_TABLE_NAME,
      'id', NEW.id,
      'operation', TG_OP
    )::TEXT
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_cache_invalidation
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION notify_cache_invalidation();

-- Application listens and clears cache
/*
// Node.js example
const client = new Client();
await client.connect();

client.on('notification', (msg) => {
  const payload = JSON.parse(msg.payload);
  console.log('Cache invalidation:', payload);

  // Clear cache
  cache.del(`user:${payload.id}`);
});

await client.query('LISTEN cache_invalidate');
*/

-- Real-time dashboard updates
CREATE OR REPLACE FUNCTION notify_metrics_update()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify(
    'metrics_update',
    json_build_object(
      'metric_name', NEW.metric_name,
      'value', NEW.value,
      'timestamp', NEW.timestamp
    )::TEXT
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER metrics_notification
AFTER INSERT ON metrics
FOR EACH ROW
EXECUTE FUNCTION notify_metrics_update();

-- Transactional notifications
BEGIN;

INSERT INTO orders (user_id, total) VALUES (123, 99.99);
-- Notification not sent yet

COMMIT;
-- Notification sent after commit

-- If rolled back:
BEGIN;
INSERT INTO orders (user_id, total) VALUES (123, 99.99);
ROLLBACK;
-- Notification NOT sent

-- Multiple channels
LISTEN orders;
LISTEN inventory;
LISTEN user_activity;

-- Conditional notifications
CREATE OR REPLACE FUNCTION notify_high_value_order()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.total > 1000 THEN
    PERFORM pg_notify(
      'high_value_order',
      json_build_object('order_id', NEW.id, 'total', NEW.total)::TEXT
    );
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
2 files · sql Explain with highlit

LISTEN/NOTIFY provides real-time pub-sub messaging. Publishers send notifications via NOTIFY. Subscribers receive notifications via LISTEN. I use it for cache invalidation, real-time updates, inter-process communication. Payloads up to 8000 bytes carry small messages. Notifications are transactional—committed before delivery. Understanding connection-based nature prevents missed messages. LISTEN/NOTIFY is simpler than external message queues for database events. Proper use enables reactive architectures. Essential for real-time features, event-driven systems, microservices coordination. PostgreSQL LISTEN/NOTIFY rivals dedicated message brokers for many use cases.