Postgres NOTIFY Payload Limits: When LISTEN/NOTIFY Is Not the Right Tool

LISTEN/NOTIFY is one of the more underused Postgres features and reaching for Redis or Kafka before checking what the database offers is a common mistake at small scale. But NOTIFY has structural limits that show up under load, and knowing them in advance saves both the architectural detour and

Postgres LISTEN/NOTIFY is a built-in publish-subscribe primitive that runs inside the database transaction model. A backend that issues NOTIFY channel, payload queues a notification that is delivered to every backend currently LISTENing on that channel after the transaction commits. The mechanism is transactional, in-process, requires no additional infrastructure, and is one of the more underused features in the database.

It is also more constrained than the documentation makes obvious at first read. The limits are not bugs—they are deliberate trade-offs that fall out of the design. Reaching for an external message broker before checking the database's own primitives is a recurring small-team mistake, but reaching for LISTEN/NOTIFY for the wrong workload is the same mistake in reverse. Both are cases of not reading the trade-offs.

The 8000-byte payload limit

The most concrete limit is that NOTIFY payloads are capped at 8000 bytes. The limit is hard-coded in the source and does not change with configuration. Payloads beyond the limit produce an error rather than a truncation, which is the right behavior—silent truncation would be substantially worse—but it means the canonical pattern of putting a complete event body in the payload does not generalize.

The right pattern when events are larger than 8000 bytes is to write the event to a table and send only the row ID in the NOTIFY payload. Listeners SELECT the row when they receive the notification. The pattern adds one round trip per event but removes the payload size limit and adds the natural durability property that the event survives a restart even if it was sent while no listener was connected. The pattern is also what most production LISTEN/NOTIFY users end up doing for reasons unrelated to the size limit.

The async queue overflow

Pending notifications for a backend are buffered in shared memory. The buffer is sized by max_notify_queue_pages (a compile-time constant in older versions, configurable in Postgres 17+). When the buffer fills, NOTIFY-issuing backends slow down to wait for slow listeners to drain. If listeners disconnect or fail to keep up, the queue can grow to the point where new transactions cannot commit.

The mode is unusual among Postgres failure modes because it propagates backward: a slow listener does not just delay its own notifications, it eventually blocks every transaction that uses NOTIFY anywhere in the cluster. The fix is monitoring pg_notification_queue_usage and treating values above a small percentage as alert-worthy.

Same-transaction ordering

Notifications issued within a single transaction are delivered to listeners after the transaction commits, in the order they were issued. Notifications across transactions interleave by commit order, not by issue order. This is usually what you want, but it has a subtle implication: a long-running transaction that issues a notification early does not have that notification delivered until the transaction commits, which can be substantially later than other transactions that issued notifications afterward.

The pattern that bites is using NOTIFY for low-latency signaling inside long transactions. The signal arrives at commit time, not at issue time, and if the transaction takes seconds the signal is seconds late. The fix is to issue notifications in short transactions or to use a different mechanism for cases where issue-time latency matters.

No durability across restart

Notifications are in-memory only. Backends that disconnect lose any pending notifications. Backends that connect after a notification was issued do not receive it. The mechanism is fundamentally fire-and-forget from the perspective of any individual listener.

For most LISTEN/NOTIFY use cases this is fine because the canonical pattern is notification-plus-state-in-database: the NOTIFY is a wakeup signal and the actual event data lives in a table that survives restart. A listener that reconnects after disconnection consults the table for events it missed. The pattern degrades NOTIFY's lossiness to a latency cost rather than a correctness cost.

When LISTEN/NOTIFY is the right tool

LISTEN/NOTIFY shines for small-to-medium event volumes where listeners are mostly connected and the events are mostly state-changes that listeners can pick up from the database. Cache invalidation, search index updates, config reload signals, and worker wakeup are the canonical fits. The pattern of small payload plus row ID plus listener fetching the row covers most of the practical use space.

The threshold where another mechanism wins is workload-dependent but typically lies around thousands of notifications per second sustained, or single notifications that need millisecond-scale delivery latency with no transaction overhead, or fanout to dozens of listener processes where the per-connection cost becomes substantial. Below those thresholds, an external broker is usually adding operational complexity that the system does not yet need.

What LISTEN/NOTIFY does not solve

Cross-database notification requires logical replication or an external broker. NOTIFY is scoped to a single Postgres cluster. Replicas do receive NOTIFY via streaming replication if configured, but the latency follows replication lag, not the original notification timing.

Notifications across multiple Postgres instances—a sharded deployment or a microservice architecture with per-service databases—does not work via NOTIFY at all. The pattern in those architectures is usually an external broker or a logical replication subscriber that fans out to other systems. Reaching for NOTIFY across instance boundaries is a sign that the architecture has outgrown the primitive.

Our use across the four products

Our SQLite-based products have no LISTEN/NOTIFY analog and rely on polling for cross-process coordination. The polling cost is fine at our scale and the simplicity is preferable to introducing a separate signaling mechanism. The Postgres migration plan includes LISTEN/NOTIFY for cache invalidation and worker wakeup—both fits for the primitive—and would not extend it to anything that approaches the scale or latency thresholds where alternatives win.

The deeper observation about LISTEN/NOTIFY is that it sits in the gap between database features that get heavy use and external infrastructure that gets reached for too quickly. Most small-team architectures could benefit from understanding it well enough to know when to use it and when not to. The trade-offs are not subtle, but they are not obvious from the documentation either—they require knowing the limits in advance rather than discovering them under production load.


Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) put these patterns into production.

Read more