LISTEN/NOTIFY is a per-session feature, and Multigres pools connections away from clients. Here's how we keep Postgres's pub/sub working when no client owns a backend session.
Postgres LISTEN / NOTIFY has a scaling problem: the more clients listen, the slower every notification gets — by orders of magnitude once you're into the thousands. Multigres keeps that line flat, even though it pools connections away from clients — the one thing the feature depends on. How it pulls that off, and the edge cases you have to get right to make it behave exactly like Postgres, is what this post is about.
LISTEN / NOTIFY is Postgres's built-in publish/subscribe mechanism. A session subscribes to a named channel:
LISTEN events;
Another session publishes to it:
NOTIFY events, 'cache invalidated: user:42' ;
Every session currently listening on events receives an asynchronous message containing the notifying backend's PID, the channel name, and the payload. On the wire, the server pushes it to the client as a NotificationResponse (message type 'A' ) outside the normal request/response flow. Applications lean on this for cache invalidation, job queues, and realtime fan-out precisely because it avoids polling.
A LISTEN registers the current session as a listener, and that registration is cleared the moment the session ends; it's per-connection state. Delivery, on the other hand, is broader: a NOTIFY reaches every session in the same database that is listening on the channel, no matter which backend connection it sits on. Postgres implements this with a single cluster-wide queue on disk ( pg_notify/ ), tags each notification with the sender's database OID to keep it database-local, and signals every listening backend to come read it. In short, registration is per-session, delivery is per-database.
Notice what Postgres ties the feature to: a session. And a pooler's whole job is to stop clients from owning one. Multigres deliberately decouples a client connection from any single Postgres backend, a client's queries can land on a different backend connection from one statement to the next. With no durable session to live in, LISTEN has nothing to attach to:
LISTEN can't run on a borrowed connection. If LISTEN events executed on whatever backend happened to be free, the very next statement could be routed elsewhere, and the registration would be stranded on a connection the client no longer holds. Notifications would arrive where the client can't see them. Postgres would faithfully deliver to whatever backend ran the LISTEN , but in a pooler that backend isn't the client's to keep. The message would land on a connection that has already been handed back to the pool and reused by someone else.
... continue reading