PGMQ Queues
pgmq turns PostgreSQL into a durable message queue: no Redis, RabbitMQ or
background worker, just tables and SQL functions in the pgmq schema.
Producers send JSON messages, consumers read them with a visibility timeout,
and messages stay in the queue until they are explicitly deleted or archived.
Install
pgcli installs the extension package from the Pigsty repository and runs
CREATE EXTENSION IF NOT EXISTS pgmq in the instance’s default database:
To use it from another database in the same instance:
Check the installed version:
Quick start
Run the same statements through pgcli:
Queues on disk
Each queue is a table in the pgmq schema:
| Object | Purpose |
|---|---|
pgmq.q_<name> |
live messages |
pgmq.a_<name> |
archive populated by pgmq.archive |
pgmq.meta |
queue registry used by list_queues and metrics |
A live message row contains msg_id, read_ct (number of deliveries),
enqueued_at, vt (visible again at) and message jsonb. Recent versions add
last_read_at and headers.
Because messages are rows, pgmq.send participates in transactions: it commits
or rolls back with the surrounding statement. Queue traffic is also ordinary
table traffic — high send/delete rates bloat the table like any other.
Sending messages
send returns the new msg_id; send_batch returns one id per message. Use
send_batch instead of looping over send — it is one round trip and the whole
batch is atomic.
Payloads must be valid JSON and are stored as jsonb: keys are reordered and
duplicates removed. Keep large blobs out of the queue.
Newer versions accept a headers jsonb argument for metadata such as a trace id
or a FIFO group key.
Reading and acknowledging
read(queue, vt, qty) returns up to qty visible messages and makes them
invisible for vt seconds. Reading does not remove anything, so each message
must be acknowledged:
pgmq.pop('jobs') reads and deletes in one call. Use it only when losing an
in-flight message on a consumer crash is acceptable.
Avoid a tight polling loop in long-running consumers:
This waits up to max_poll_seconds (5) for messages, checking every
poll_interval_ms (100) milliseconds, and returns as soon as a message arrives.
Visibility timeout and retries
The visibility timeout is PGMQ’s delivery model:
readsetsvt = now() + timeoutand incrementsread_ct.- The message stays invisible to other consumers until
vt. deleteorarchiveremoves it. If the consumer crashes or does not acknowledge in time, the message becomes visible again aftervtand is delivered to another consumer.
Delivery is therefore at-least-once across failures. Choose vt longer than
the worst-case processing time, and make consumers idempotent. For long jobs,
extend the lease without re-reading:
read_ct is a ready-made retry counter.
Archive and dead letters
delete is permanent. archive moves a message to pgmq.a_<queue> for
retention, audit or replay:
A simple dead-letter pattern is to archive messages that failed too many times:
Without a rule like this, a poison message is redelivered forever.
Monitoring
metrics reports queue_length, queue_visible_length, the age of the newest
and oldest messages, and total_messages. Older extension versions return a
smaller set of columns.
The queue table can be queried directly:
Alert when queue_length keeps growing or oldest_msg_age_sec climbs: either
consumers are down or they cannot keep up.
Maintenance
create_unloggedtrades crash safety for write speed. Use it only for data that can be regenerated.create_partitionedrequires thepg_partmanextension. It keeps a very high-throughput queue from bloating a single table; old partitions are dropped automatically.purge_queueempties a queue but keeps it;drop_queueremoves it entirely.- Queue names are limited to 47 characters.
With ordinary queues, heavy send/delete traffic produces dead tuples. Make sure autovacuum keeps up, or archive messages instead of deleting them and consider partitioned queues at high volume.
Permissions
PGMQ functions run with the caller’s privileges. An application role needs access to the schema, the functions and the queue tables:
Run pgmq.create as the queue owner; creating tables requires privileges the
application role usually should not have. New queues need their own grants, or
you can pre-configure default privileges.
Function reference
| Function | Purpose |
|---|---|
pgmq.create(name) |
Create a queue |
pgmq.create_unlogged(name) |
Create an unlogged queue |
pgmq.create_partitioned(name, ...) |
Create a partitioned queue |
pgmq.send(name, msg [, delay]) |
Send one message, returns msg_id |
pgmq.send_batch(name, msgs jsonb[]) |
Send a batch atomically |
pgmq.read(name, vt, qty) |
Read and hide messages |
pgmq.read_with_poll(name, vt, qty, ...) |
Long-poll read |
pgmq.pop(name [, qty]) |
Read and delete in one call |
pgmq.delete(name, id) / pgmq.delete(name, ids[]) |
Acknowledge and remove |
pgmq.archive(name, id) / pgmq.archive(name, ids[]) |
Move to the archive table |
pgmq.set_vt(name, id(s), seconds) |
Extend the visibility timeout |
pgmq.list_queues() / pgmq.metrics(name) |
Inspect queues |
pgmq.purge_queue(name) |
Delete all messages, keep the queue |
pgmq.drop_queue(name) |
Remove the queue and its archive |
Troubleshooting
extension "pgmq" is not available. The extension package is not in the
instance image, or the extension has no build for this database. Install it with
pg extension install pgmq -i <instance> and create it in the target database.
Messages are redelivered. Processing took longer than vt, or the consumer
exited before delete. Increase vt, call set_vt during long jobs, and make
processing idempotent.
Queue length never drops. Consumers are failing or too slow. Inspect
read_ct, application logs and metrics().oldest_msg_age_sec. PGMQ retries
failed messages forever unless they are archived or deleted.
Performance degrades over time. Default queues are ordinary tables. Check
dead tuples and autovacuum, batch sends and reads, and consider
create_partitioned for sustained high throughput.
permission denied for schema pgmq. Grant USAGE on the schema and
EXECUTE on the functions to the application role, plus table privileges on
each queue it uses.