The PgBouncer trap you found for LISTEN/NOTIFY has a second victim I hadn't thought about until reading this: the advisory-lock connection for the poller failover. pg_try_advisory_lock is scoped to the physical session, same as LISTEN. If that connection ever gets routed through a transaction-mode pooler instead of staying direct, the lock either evaporates between polls or two instances both end up believing they hold it, for the exact same reason the notifications went missing. It's the kind of thing that passes every test because nobody adds the pooler until months later, for a completely unrelated reason, and then the poller starts double-processing with no error anywhere. Worth a comment right on POLLER_LOCK_KEY warning future-you not to move that connection behind a pooler.
The PgBouncer trap you found for LISTEN/NOTIFY has a second victim I hadn't thought about until reading this: the advisory-lock connection for the poller failover. pg_try_advisory_lock is scoped to the physical session, same as LISTEN. If that connection ever gets routed through a transaction-mode pooler instead of staying direct, the lock either evaporates between polls or two instances both end up believing they hold it, for the exact same reason the notifications went missing. It's the kind of thing that passes every test because nobody adds the pooler until months later, for a completely unrelated reason, and then the poller starts double-processing with no error anywhere. Worth a comment right on POLLER_LOCK_KEY warning future-you not to move that connection behind a pooler.