backend

Postgres LISTEN/NOTIFY for Job Queues: 2026 Guide

June 11, 2026

Postgres LISTEN/NOTIFY for Job Queues: 2026 Guide

Use Postgres LISTEN/NOTIFY only as the wake-up signal for a job queue, never as the queue itself. Notifications aren't persisted, payloads cap at 8,000 bytes, and NOTIFY serializes commits under load. Keep jobs in a table and claim them with FOR UPDATE SKIP LOCKED.

TL;DR

LISTEN/NOTIFY is Postgres's built-in pub/sub signal, and it is tempting to build a job queue on it because it removes polling. The problem: notifications vanish if a listener is disconnected, every commit that carries a NOTIFY takes a global lock that serializes all commits on the instance, and LISTEN breaks behind transaction-mode PgBouncer. The production-safe pattern in 2026 is a jobs table drained with SELECT ... FOR UPDATE SKIP LOCKED, with NOTIFY used only to wake idle workers — or an off-the-shelf extension like pgmq.

What you'll learn

  • How LISTEN/NOTIFY actually works, including its transaction and delivery semantics
  • Why LISTEN/NOTIFY does not scale under many concurrent writers (the "database 0" global lock)
  • The hard limits: 8,000-byte payloads, the 8GB notification queue, and lost messages on disconnect
  • Why LISTEN fails behind PgBouncer transaction pooling, and what to do about it
  • The SKIP LOCKED job-queue pattern and ready-made alternatives like pgmq
  • When LISTEN/NOTIFY is still the right tool

What is LISTEN/NOTIFY in Postgres and how does it work?

LISTEN/NOTIFY is Postgres's interprocess notification mechanism: a session runs LISTEN channel, and any session that runs NOTIFY channel, 'payload' in the same database pushes an event to every current listener on that channel. It is push-based, so listeners don't poll.

Three semantics matter for queue builders. First, a NOTIFY issued inside a transaction is delivered only if and when that transaction commits — aborted transactions send nothing.1 Second, identical channel-plus-payload notifications inside one transaction are folded into a single delivered event, while distinct payloads are delivered separately.1 Third, a transaction that has executed NOTIFY cannot be prepared for two-phase commit.1 For dynamic channel names, the function form pg_notify(channel, payload) is easier to use than the bare SQL command.1

-- worker session
LISTEN job_created;

-- producer session
INSERT INTO jobs (kind, args) VALUES ('send_email', '{"to": "..."}');
NOTIFY job_created, '42';  -- or: SELECT pg_notify('job_created', '42');

Why does Postgres LISTEN/NOTIFY not scale?

Because any commit containing a NOTIFY takes a global AccessExclusiveLock on the Postgres instance while it appends to the notification queue, which serializes those commits — with enough concurrent writers, the whole database stalls. The lock shows up in logs as AccessExclusiveLock on object 0 of class 1262 of database 0, and the Postgres source explains it exists to guarantee queue entries appear in commit order.2

This is not theoretical. Recall.ai, which writes structured meeting data from tens of thousands of simultaneous producers, traced three production outages between March 19 and March 22, 2025 to exactly this lock: database load spiked while CPU and disk I/O plummeted — the signature of lock contention rather than real work. Removing the single NOTIFY-carrying code path fixed it, and their write-up concludes bluntly that you shouldn't use LISTEN/NOTIFY with many concurrent writers.2

There is good news on the horizon: a core fix (commit 282b1cde by Joel Jacobson, committed by Tom Lane) eliminates this bottleneck, but it is committed for PostgreSQL 19 — which reached Beta 1 on June 4, 2026, with GA expected around September 2026 — and is not in any production release. The newest stable releases as of May 2026 are 18.4, 17.10, 16.14, 15.18, and 14.23.345 If you run a released Postgres today, the global lock is still your reality.

What is the NOTIFY payload size limit?

8,000 bytes in the default configuration.1 That ceiling is why the standard advice is to never put job data in the payload: send only an identifier (or nothing at all) and keep the actual job row in a table. The official docs say the same — for large or binary data, "put it in a database table and send the key of the record."1

The notification queue that buffers undelivered events is also finite: about 8GB in a standard installation, configurable via max_notify_queue_pages since PostgreSQL 17. If the queue fills, transactions calling NOTIFY fail at commit, and Postgres starts logging warnings once it is half full — usually pointing at a listener stuck in a long-running transaction that blocks cleanup. You can monitor usage with pg_notification_queue_usage().16

What happens to notifications if a listener disconnects?

They are lost permanently. Notifications are delivered only to sessions currently listening; nothing is stored for a listener that is disconnected, restarting, or crashed, and there is no replay mechanism.1 This is the single most important reason LISTEN/NOTIFY cannot be the queue itself: a deploy, a network blip, or a worker OOM means silently dropped jobs.

The standard mitigation is to make the table the source of truth. Workers always do a catch-up query on startup (and periodically), so a missed notification only delays a job until the next sweep instead of losing it.

Does LISTEN/NOTIFY work with PgBouncer or connection pooling?

LISTEN does not work through PgBouncer in transaction pooling mode — the mode most production deployments use. A listener needs a stable server connection to keep receiving events, and transaction pooling hands the server connection back to the pool after every transaction.7 Sending with NOTIFY is fine in transaction mode, since it's an ordinary statement.8

The workaround: give listeners a dedicated path — either a separate PgBouncer database entry configured for session pooling or a direct connection to Postgres — and keep one listening connection per worker process, not per pooled client. The same constraint applies to most transaction-mode poolers, and supporting LISTEN in that mode remains an open feature request in PgBouncer.8 For a deeper look at pooling modes and their trade-offs, see our production Postgres pooling guide with PgBouncer and Supavisor.

What should you use instead of LISTEN/NOTIFY for a job queue?

Use a jobs table claimed with FOR UPDATE SKIP LOCKED — available since PostgreSQL 9.5 — as the backbone, and add NOTIFY only as a wake-up optimization if polling latency bothers you.9 SKIP LOCKED lets many workers pull from the same table without blocking each other: rows another worker has locked are simply skipped, which the Postgres docs explicitly call out as the pattern for "multiple consumers accessing a queue-like table."10

UPDATE jobs
SET status = 'running', started_at = now()
WHERE id = (
  SELECT id FROM jobs
  WHERE status = 'queued'
  ORDER BY created_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
RETURNING *;

Jobs survive crashes because they are ordinary rows; retries, priorities, and dead-letter handling are just columns and queries; and everything is transactional with your business writes — the property that makes Postgres queues attractive in the first place.

If you'd rather not hand-roll it, pgmq is an open-source extension that implements an SQS-like queue (exactly-once delivery within a visibility timeout) on Postgres 14 through 18, and is used by Supabase and Tembo among others.11 In Node, pg-boss gives you a production-ready Postgres job queue built on exactly these primitives. And if you genuinely need six-figure messages per minute, fan-out to many consumers, or cross-service delivery, that is the point where a dedicated broker (SQS, RabbitMQ, Kafka, Redis Streams) earns its operational cost.

When is LISTEN/NOTIFY still the right choice?

When write volume is modest, listeners hold direct (or session-pooled) connections, and a missed notification is harmless. Good fits include cache invalidation signals, "config changed, reload" pings, live dashboard refreshes, and waking a small pool of queue workers so they poll immediately instead of on the next tick. In all of these, the notification is a hint, not the data — if it's lost, a periodic sweep or the next poll catches up. We walk through a healthy use of the feature in our real-time presence tutorial with LISTEN/NOTIFY. If any of those three conditions fails — many concurrent NOTIFY-carrying writers, transaction-pooled connections, or correctness depending on delivery — reach for the table-based pattern instead.

Bottom line

LISTEN/NOTIFY is a notification primitive, not a queue. Keep jobs in a table, claim them with SKIP LOCKED, and let NOTIFY do the one thing it's good at: telling an idle worker to check now rather than in a second. If you're ready to build, start with the pg-boss Postgres job queue tutorial, and make sure your connection layer is solid with the PgBouncer and Supavisor pooling guide.

Footnotes

  1. PostgreSQL 18 official documentation, NOTIFY — https://www.postgresql.org/docs/current/sql-notify.html 2 3 4 5 6 7 8

  2. Recall.ai, "Postgres LISTEN/NOTIFY does not scale" (updated May 8, 2026) — https://www.recall.ai/blog/postgres-listen-notify-does-not-scale 2

  3. Robins Tharakan, "Turbocharging LISTEN/NOTIFY with 40x Boost" (Jan 2026) — https://www.robins.in/2026/01/turbocharging-listennotify-with-40x.html

  4. PostgreSQL 18.4, 17.10, 16.14, 15.18, and 14.23 released (May 14, 2026) — https://www.postgresql.org/about/news/postgresql-184-1710-1614-1518-and-1423-released-3297/

  5. PostgreSQL 19 Beta 1 released (June 4, 2026) — https://www.postgresql.org/about/news/postgresql-19-beta-1-released-3313/

  6. max_notify_queue_pages parameter reference — https://postgresqlco.nf/doc/en/param/max_notify_queue_pages/

  7. PgBouncer features — https://www.pgbouncer.org/features.html

  8. PgBouncer issue #655, "Feature: Listen/Notify Support with Transaction Pooling" — https://github.com/pgbouncer/pgbouncer/issues/655 2

  9. PostgreSQL wiki, "What's new in PostgreSQL 9.5" — https://wiki.postgresql.org/wiki/What's_new_in_PostgreSQL_9.5

  10. PostgreSQL 18 official documentation, SELECT (locking clause) — https://www.postgresql.org/docs/current/sql-select.html

  11. pgmq — A lightweight message queue. Like AWS SQS and RSMQ but on Postgres — https://github.com/pgmq/pgmq

Frequently Asked Questions

Not by itself. Notifications are unpersisted (disconnected listeners lose them), payloads are capped at 8,000 bytes, and NOTIFY serializes commits under load. Use a jobs table with SKIP LOCKED; use NOTIFY only to wake workers.