Open source PostgreSQL queue

Postgres is already your message queue.

pg-queue turns the database you already run into a durable queue. Instant delivery over LISTEN/NOTIFY, lock-free work-stealing with FOR UPDATE SKIP LOCKED, and ACID guarantees because your messages are just rows. No Redis, no broker to operate.

MIT licensed. TypeScript-first. Works with any PostgreSQL 13 or newer.

The message lifecycle

From enqueue to done, all in one database

The whole loop runs in PostgreSQL. No broker to deploy, no second system to back up or monitor. Just a table, a trigger, and your workers.

  1. 01 Enqueue a row A message is a row in a table pg-queue owns. Enqueue it on its own, or inside the same transaction as your domain writes for a transactional outbox.
  2. 02 Workers wake instantly An insert trigger fires LISTEN/NOTIFY, so idle workers wake in milliseconds. No busy polling, no fixed tick to wait for.
  3. 03 Steal work, lock-free Each worker claims the next message with FOR UPDATE SKIP LOCKED. Many workers pull at once with no double-processing and no locks held across your handler.
  4. 04 Ack, retry, or dead-letter A handler that returns acks the message. A failure retries with exponential backoff, and once it is out of attempts it lands in the dead letter queue for you to inspect.

What pg-queue gives you

A real queue, without a new system to run

Everything a background-job system needs, delivered as a library on top of PostgreSQL. You get the guarantees of a broker with the operations of a table.

Instant delivery

An insert trigger fires LISTEN/NOTIFY, so workers pick up new messages the moment they land instead of waiting for the next poll. Idle workers cost nothing.

Lock-free work-stealing

FOR UPDATE SKIP LOCKED lets any number of workers claim messages concurrently. No worker blocks on another, and no message is processed twice.

Transactional enqueue

Enqueue inside your own transaction with withTransaction. The message commits only if your domain write does, which gives you a transactional outbox with no extra moving parts.

Retries and a dead letter queue

Failed messages retry with exponential backoff up to a limit you set, then move to a dead letter status you can inspect, replay, or clean up.

Priority and concurrency

Ten priority levels order the work, a concurrency setting bounds how much runs at once, and graceful shutdown drains in-flight messages before exit.

Zero extra infrastructure

No Redis, no broker, no sidecar. pg-queue creates its own table on first use and can share the connection pool your app already has.

Why a database makes a good queue

Fewer moving parts, stronger guarantees.

Reaching for Redis or a broker means a second system to run and a gap between your data and your messages. Keeping the queue in Postgres closes that gap.

01

One database, not two

The backups, failover, monitoring, and access control you already run for Postgres now cover your queue too. There is no second system to operate, secure, or page someone about at 3am.

02

ACID, not at-most-once

Messages are rows, so an enqueue can ride inside the same transaction as your domain write. Either both commit or neither does. No lost events, no message that points at a row that never got saved.

03

Lock-free at scale

SKIP LOCKED lets workers share the load with no contention, and NOTIFY removes the polling tax. Postgres is doing what it is already very good at: concurrent access to rows.

Where pg-queue fits

Built for the work most apps actually queue

If you already run PostgreSQL, most queueing needs are a library away. These are the patterns pg-queue was built to handle.

Transactional outbox

Save an order and enqueue the "order placed" event in one transaction. Downstream work fires only if the write committed, so events never drift from your data.

Background jobs

Push email, exports, thumbnails, and report generation off the request path. The user gets a fast response, the work happens on your workers.

Webhook delivery

Queue outbound webhooks and let the built-in retries and backoff handle flaky receivers, with anything that never succeeds landing in the dead letter queue.

Fan-out to workers

Spread a burst of work across many workers with bounded concurrency. SKIP LOCKED keeps them from stepping on each other.

Deferred and retryable steps

Multi-step flows where a step can fail and should retry later: charge a card, sync an integration, reconcile a record, without bespoke retry plumbing.

Skip the broker

For small and mid-size workloads, replace a Redis or RabbitMQ dependency entirely. One less service to run, secure, and pay for.

Get started

Put the database you already run to work.

Install pg-queue, point it at your connection string, and you have a durable queue in a few lines. No broker to stand up, no new service to operate.

$ npm install @systeric/pg-queue