Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/NikolayS/PgQue/llms.txt

Use this file to discover all available pages before exploring further.

When a consumer encounters a processing failure, pgque.nack() schedules the message for redelivery after a configurable delay. Once a message has been nacked more times than the queue’s max_retries setting, it is routed to the dead letter queue (pgque.dead_letter) instead of being retried. This guide covers the full failure-handling lifecycle.

The nack and retry flow

The key pattern: call nack() for each failed message, then call ack() to close the batch. Both are required.
do $$
declare
    v_msg pgque.message;
begin
    select * into v_msg from pgque.receive('orders', 'processor', 1) limit 1;

    -- nack re-queues the message for retry after 5 minutes
    perform pgque.nack(v_msg.batch_id, v_msg, interval '5 minutes', 'validation failed');

    -- ack closes the batch and advances the consumer cursor
    perform pgque.ack(v_msg.batch_id);
end $$;
Both nack() and ack() are required. nack() schedules per-message retry; ack() closes the batch and advances the consumer cursor. Without ack(), the same batch is re-delivered on every receive() call forever.
After nacking, the message sits in PgQ’s internal retry queue. pgque.maint_retry_events() (scheduled every 30 seconds by pgque.start()) moves due rows back into the main event stream. The next tick then makes them visible to consumers.
The maint_retry_events()ticker()receive() chain must run in separate transactions, just like the normal send/tick/receive flow. The snapshot rule applies here too.

Configuring max retries

Set max_retries per queue using set_queue_config:
-- Allow up to 5 retries before routing to the DLQ
select pgque.set_queue_config('orders', 'max_retries', '5');
The retry_count field on received messages is NULL on first delivery and increments on each retry. When nack() is called and ev_retry >= max_retries, the message goes to pgque.dead_letter instead of the retry queue.

Dead letter queue

Inspecting the DLQ

select dl_id, dl_reason, ev_id, ev_type, ev_retry, ev_data
from pgque.dlq_inspect('orders');
-- returns up to 100 rows, newest first

-- Inspect more rows
select dl_id, dl_reason, ev_id, ev_type, ev_retry, ev_data
from pgque.dlq_inspect('orders', 500);

Replaying DLQ entries

Replay a single entry by its dl_id — it re-enters the main queue with a fresh event id:
select pgque.dlq_replay(42);  -- returns the new ev_id
Replay all entries for a queue at once:
select replayed, failed, first_error
from pgque.dlq_replay_all('orders');
dlq_replay_all isolates per-event failures — one bad row does not abort the rest. Failed entries are counted in failed; first_error carries the first failure’s details.

Purging the DLQ

dlq_purge is destructive. Audit entries with dlq_inspect before purging.
-- Delete entries older than 7 days
select pgque.dlq_purge('orders', interval '7 days');

-- Delete all entries for a queue
select pgque.dlq_purge('orders', interval '0 seconds');

The dead_letter table schema

ColumnTypeNotes
dl_idbigserialPrimary key
dl_queue_idint4FK to pgque.queue
dl_consumer_idint4FK to pgque.consumer
dl_timetimestamptzWhen the message was dead-lettered
dl_reasontextReason passed to nack()
ev_idbigintOriginal event id
ev_timetimestamptzOriginal event creation time
ev_txidxid8Transaction id of the original insert
ev_retryint4Retry count when dead-lettered
ev_typetextEvent type
ev_datatextEvent payload
ev_extra1..4textExtra columns
pgque_reader has select on this table; pgque_admin has full access.

Build docs developers (and LLMs) love