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.

This guide walks the full PgQue message loop — create a queue, send an event, advance the queue, receive the batch, and acknowledge it. All examples use plain SQL and work against any Postgres 14+ instance. You will need psql and a database with PgQue installed.
The snapshot rule. send, ticker, and receive must each run in their own committed transaction. This is a hard requirement of PgQ’s snapshot-based design, not a recommendation. The ticker captures a snapshot of committed transaction IDs; an event that was sent in the same transaction as the tick is still in-progress at that moment and gets excluded from the batch’s visibility window, so it never surfaces to consumers. In psql with autocommit enabled (the default), each select statement is its own transaction — do not wrap the steps below in a single begin/commit block.
1

Install PgQue

If you have not installed PgQue yet, follow the installation guide. Come back here once select pgque.version(); returns a version string.
2

Create a queue and consumer

A queue is a named, shared event log. A consumer is a named cursor into that log. Any number of producers can write to the same queue concurrently, and any number of consumers can subscribe — each sees every event independently (fan-out by default).
-- tx 1: create queue + consumer
select pgque.create_queue('orders');
select pgque.subscribe('orders', 'processor');
create_queue returns 1 when it creates the queue and 0 if the queue already existed — the call is idempotent. subscribe is the modern alias for register_consumer.
3

Send a message

Send one event to the queue. The jsonb overload validates and canonicalizes the payload before storing it.
-- tx 2: send a message
select pgque.send('orders', '{"order_id": 42, "total": 99.95}'::jsonb);
 send
------
    1
The return value is the event id — unique within the queue, monotonically increasing within a rotation window. If you call receive right now you will get zero rows, because no tick has run yet and no batch boundary exists. Continue to the next step.
4

Advance the queue

PgQue is tick-based, not row-claiming. Consumers do not see events directly — they see batches. A batch is the set of events between two consecutive ticks. Until a tick runs, there is no batch boundary and receive has nothing to return.In production, pg_cron or an external scheduler drives ticks automatically. For demos, tests, and manual operation, use force_next_tick to bypass the tick thresholds for one queue, then call ticker() to materialise the tick.
-- tx 3: advance the queue
-- Each select below is its own implicit transaction in psql autocommit.
-- Do NOT wrap these in begin/commit (the tick must see the send committed).
select pgque.force_next_tick('orders');
select pgque.ticker();
force_next_tick bumps the event-sequence threshold so the next ticker() call creates a tick even if the normal count/lag thresholds have not been reached. ticker() returns the number of queues it processed.
In production, pgque.start() schedules pg_cron to call pgque.ticker_loop() every second; that procedure re-ticks every 100 ms (10 ticks/sec) by default. You never need force_next_tick in a live environment.
5

Receive the batch

Now pull the batch. Each returned row is a pgque.message composite carrying msg_id, batch_id, type, payload, retry_count, created_at, and four free-form extra1..4 columns.
-- tx 4: receive — every returned row carries the same batch_id
select * from pgque.receive('orders', 'processor', 100);
 msg_id | batch_id |  type   |             payload              | retry_count | created_at
--------+----------+---------+----------------------------------+-------------+------------------------
      1 |        1 | default | {"total": 99.95, "order_id": 42} |             | 2026-04-17 10:00:00+00
(1 row)
retry_count is NULL on first delivery. The batch_id is what you need for the next step. Every row in a batch carries the same batch_id.Note that payload is stored as text; cast to jsonb for JSON field access: (payload::jsonb)->>'order_id'.
6

Acknowledge the batch

A batch stays assigned to the consumer until it calls ack. Until then, the same batch is returned every time you call receive — the consumer has not moved its cursor forward.Capture the batch_id from the previous step and acknowledge it. In psql, \gset is the ergonomic way:
-- tx 5: ack — scriptable psql idiom
select batch_id from pgque.receive('orders', 'processor', 100) limit 1 \gset
select pgque.ack(:batch_id);
Or, if you already know batch_id = 1 from the output above:
select pgque.ack(1);
ack returns 1 on success. After acking, receive returns zero rows — the consumer’s cursor has advanced past the batch.In application code, capture batch_id from any row returned by receive and pass it to ack once all events in the batch are processed.

Complete example

Here is the full loop in one block for reference. Each statement is a separate transaction in psql autocommit.
-- tx 1: create queue + consumer
select pgque.create_queue('orders');
select pgque.subscribe('orders', 'processor');

-- tx 2: send a message
select pgque.send('orders', '{"order_id": 42, "total": 99.95}'::jsonb);

-- tx 3: advance the queue if you are not using pg_cron
-- force_next_tick bumps the event-seq threshold; ticker() then inserts the tick.
-- Each select below is its own implicit transaction in psql autocommit —
-- do NOT wrap these in begin/commit (the tick must see the send committed).
select pgque.force_next_tick('orders');
select pgque.ticker();

-- tx 4: receive — every returned row carries the same batch_id
select * from pgque.receive('orders', 'processor', 100);
--  msg_id | batch_id |  type   |             payload              | retry_count | ...
-- --------+----------+---------+----------------------------------+-------------+----
--       1 |        1 | default | {"total": 99.95, "order_id": 42} |             |

-- tx 5: ack the batch_id from the previous result
select pgque.ack(1);

Next steps

Fan-out

Register multiple consumers on the same queue and deliver every event to each independently.

Retry and dead letter queue

Use nack to schedule retries and route exhausted messages to the DLQ.

Ticker and scheduling

Configure pg_cron, pg_timetable, or an external scheduler for production use.

Roles and security

Understand the producer/consumer role split and grant access safely.

Build docs developers (and LLMs) love