Postgres LISTEN/NOTIFY: fine until it isn't, and here's the cliff

4 min read 1 source explainer
├── "Postgres LISTEN/NOTIFY scales further than its reputation suggests — good enough for most application-tier pub/sub"
│  └── KraftyOne (DBOS) (DBOS Blog, 246 pts) → read

DBOS benchmarks show a single Postgres instance handling ~10,000 notifications/second with sub-10ms latency, and listeners scaling nearly linearly to 1,000 concurrent LISTENers. Their argument is that this comfortably exceeds where most application pub/sub actually operates, so reaching for Kafka/Redis/NATS by default is often premature optimization.

├── "The real bottleneck is the sender-side NotifyQueueLock, which serializes unrelated transactions"
│  └── KraftyOne (DBOS) (DBOS Blog) → read

DBOS's teardown pinpoints the ceiling as a specific implementation detail: every NOTIFY participates in its transaction, and at commit time Postgres takes an exclusive NotifyQueueLock to append to the SLRU notify queue. Under load this serializes otherwise-independent transactions and inflates commit latency system-wide, not just for notifiers.

└── "The decade-long 'ship it' vs 'never use it' debate has been unproductive because no one published numbers"
  └── top10.dev editorial (top10.dev) → read below

The editorial frames LISTEN/NOTIFY discourse as two camps talking past each other — 'free pub/sub, ship it' vs 'don't, use Redis/NATS/Kafka' — with no empirical grounding. DBOS's real contribution isn't the mechanism (which is documented in the Postgres source) but publishing the actual shape of the scaling curve so engineers can make informed tradeoffs.

What happened

DBOS published a benchmark of Postgres's built-in `LISTEN`/`NOTIFY` pub/sub, and the headline is more interesting than the usual "Postgres can do everything" post. On a single Postgres instance they pushed roughly 10,000 notifications per second before throughput flatlined, with sub-10ms delivery latency at reasonable loads. That's not Kafka territory, but it's comfortably past the point where most application-tier pub/sub actually lives.

The post walks through three axes: number of listeners, notification rate, and payload size. Listeners scale nearly linearly — the server fans out cheaply, and 1,000 concurrent `LISTEN`ers barely move the needle. Payload size matters less than you'd think until you approach the hard 8,000-byte limit per notification. The real ceiling shows up on the sender side, and the reason is a specific implementation detail rather than a vague "Postgres doesn't scale for this" hand-wave.

Specifically: every `NOTIFY` participates in the transaction that issued it, and at commit time Postgres takes an exclusive lock (`NotifyQueueLock`) to append entries to the shared notify queue in SLRU. Under contention, transactions that would otherwise be independent end up serialized behind that lock. The listeners aren't the bottleneck — the writers are, and the moment your NOTIFY volume climbs, commit latency on unrelated transactions climbs with it.

Why it matters

LISTEN/NOTIFY has been in Postgres since the 7.x days, and its reputation lives in two extremes. In one camp: "free pub/sub, ship it." In the other: "don't, it doesn't scale, use Redis / NATS / Kafka." Both camps have been arguing past each other for a decade because nobody bothered to publish numbers on where the actual cliff is. DBOS's contribution isn't the mechanism — that's well-documented in the Postgres source — it's the shape of the curve.

The HN thread on the post is worth reading precisely because it surfaces the war stories. Multiple commenters report running LISTEN/NOTIFY happily in production at hundreds of notifications per second for years. Others describe hitting the wall painfully — one recounts a system where a background job burst caused NOTIFY commit contention to spike p99 write latency across the whole database, including the checkout path. Both accounts are true. The difference is entirely about whether your NOTIFY-issuing transactions overlap with your hot write path.

Compare this to the standard alternatives. Redis pub/sub is faster and lock-free, but it's fire-and-forget — a disconnected subscriber misses everything, and you get zero delivery guarantees. NATS and Kafka give you durability and much higher throughput but drag in operational complexity that a small team feels immediately. LISTEN/NOTIFY's real advantage isn't performance; it's transactional coupling — the notification fires if and only if the transaction commits, and never fires twice. For workflows like "job inserted into queue table → wake up worker," that atomicity is worth a lot. You don't need a separate outbox pattern. You don't need to reason about the ordering of a Redis publish and a database commit. The listener sees the row because the commit that made the row also made the notification.

The other honest caveat DBOS surfaces: notifications are delivered per-connection, and if a listener falls behind, notifications queue in memory on the backend serving that connection. Slow consumers become memory pressure on Postgres itself, which is a failure mode most people don't think about until pg's RSS is doing something alarming at 3am.

What this means for your stack

If you're currently running Redis solely for pub/sub against a Postgres-backed app, this benchmark is a real prompt to reconsider. One less service, one less deploy, one less thing to page on. The transactional guarantee alone eliminates a category of bugs — the classic "we published to Redis but the transaction rolled back" race disappears entirely. For workflow engines, cache invalidation fanout, and "tell workers there's new work" patterns, LISTEN/NOTIFY is genuinely the right tool.

The rule of thumb that falls out of the numbers: keep NOTIFY off the transactions that are already your bottleneck. If your checkout path is 40% of database CPU, don't add a NOTIFY to it — the exclusive lock will make a bad problem worse. Push the notification into a downstream transaction, a trigger on a low-traffic audit table, or a dedicated "events" table with a separate worker that batches NOTIFYs. The pattern that works: a small pool of dedicated "notifier" connections that do nothing except fan out events, keeping the lock contention isolated from customer-facing writes.

On the listener side, treat every LISTEN connection as a long-lived resource with a bounded consumer. If you can't process a notification in under a few milliseconds, drop it into an in-memory queue and ack quickly — otherwise you're building up state on the Postgres backend rather than in your application, and that's the wrong place for it. Reconnect logic needs to assume missed notifications: on reconnect, always do a catch-up query against the source of truth. LISTEN/NOTIFY is a wakeup mechanism, not a message bus.

Looking ahead

The interesting question is whether the Postgres core team ever revisits `NotifyQueueLock`. There have been threads on pgsql-hackers about partitioning the notify queue or moving to a lock-free ring buffer, but nothing has landed and the pressure to change it is low precisely because most workloads never hit the ceiling. For now, the takeaway is boring and useful: LISTEN/NOTIFY is a real tool with a knowable performance envelope. Measure your NOTIFY rate, keep it off your hot writes, and you can retire a whole class of infrastructure. Benchmark it against your actual workload before you either dismiss it or bet the business on it.

Hacker News 278 pts 49 comments

Postgres LISTEN/NOTIFY actually scales

→ read on Hacker News

// share this

// get daily digest

Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.