A lightweight message queue. Like AWS SQS and RSMQ but on Postgres.
Documentation: https://pgmq.github.io/pgmq/
Source: https://github.com/pgmq/pgmq
- Lightweight - No background worker or external dependencies, just Postgres SQL objects
- Guaranteed "exactly once" delivery of messages to a consumer within a visibility timeout
- API parity with AWS SQS and RSMQ
- FIFO (First-In-First-Out) queues with message group keys for ordered processing
- Topic-based routing with wildcard patterns for publish-subscribe and content-based routing
- Messages stay in the queue until explicitly removed
- Messages can be archived, instead of deleted, for long-term retention and replayability
Supported on Postgres 14-18.
PGMQ can be run on any existing Postgres instance or installed as a Postgres Extension. See INSTALLATION.md for the full installation guide including a comparison of the Postgres Extension vs the SQL-only installation.
The fastest way to get started is by running the Docker image, where PGMQ comes pre-installed as an extension in Postgres.
docker run -d --name pgmq-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 ghcr.io/pgmq/pg18-pgmq:v1.10.0Then connect and enable PGMQ:
psql postgres://postgres:postgres@localhost:5432/postgresCREATE EXTENSION pgmq;You can also use psql to install PGMQ's objects directly into the pgmq schema in Postgres. Use this method if you are running someplace that does not natively support the PGMQ Extension. Read these considerations before you decide.
git clone https://github.com/pgmq/pgmq.git
cd pgmq
psql -f pgmq-extension/sql/pgmq.sql postgres://postgres:postgres@localhost:5432/postgresTo update PGMQ versions, follow the instructions in UPDATING.md.
Community
- .NET
- Dart
- Elixir + Broadway
- Elixir
- Go
- Haskell
- Java (JDBC)
- Java (Spring Boot)
- Javascript (NodeJs)
- Kotlin JVM (JDBC)
- Kotlin Multiplatform (sqlx4k)
- PHP (non blocking)
- Python (with SQLAlchemy)
- REST-API (Bun + Elysia)
- Ruby
- TypeScript (Deno)
- TypeScript (NodeJs + Prisma)
- TypeScript (NodeJs + Midway.js)
# Connect to Postgres
psql postgres://postgres:[email protected]:5432/postgres-- create the extension in the "pgmq" schema
CREATE EXTENSION pgmq;Every queue is its own table in the pgmq schema. The table name is the queue name prefixed with q_.
For example, pgmq.q_my_queue is the table for the queue my_queue.
-- creates the queue
SELECT pgmq.create('my_queue'); create
-------------
(1 row)
-- messages are sent as JSON
SELECT * from pgmq.send(
queue_name => 'my_queue',
msg => '{"foo": "bar1"}'
);The message id is returned from the send function.
send
-----------
1
(1 row)
-- Optionally provide a delay
-- this message will be on the queue but unable to be consumed for 5 seconds
SELECT * from pgmq.send(
queue_name => 'my_queue',
msg => '{"foo": "bar2"}',
delay => 5
); send
-----------
2
(1 row)
Read 2 message from the queue. Make them invisible for 30 seconds.
If the messages are not deleted or archived within 30 seconds, they will become visible again
and can be read by another consumer.
SELECT * FROM pgmq.read(
queue_name => 'my_queue',
vt => 30,
qty => 2
); msg_id | read_ct | enqueued_at | last_read_at | vt | message | headers
--------+---------+-------------------------------+-------------------------------+-------------------------------+-----------------+---------
1 | 1 | 2026-01-23 20:27:21.7741-06 | 2026-01-23 20:27:31.605236-06 | 2026-01-23 20:28:01.605236-06 | {"foo": "bar1"} |
2 | 1 | 2026-01-23 20:27:26.505063-06 | 2026-01-23 20:27:31.605252-06 | 2026-01-23 20:28:01.605252-06 | {"foo": "bar2"} |
If the queue is empty, or if all messages are currently invisible, no rows will be returned.
SELECT * FROM pgmq.read(
queue_name => 'my_queue',
vt => 30,
qty => 1
); msg_id | read_ct | enqueued_at | last_read_at | vt | message | headers
--------+---------+-------------+--------------+----+---------+---------
-- Read a message and immediately delete it from the queue. Returns an empty record if the queue is empty or all messages are invisible.
SELECT * FROM pgmq.pop('my_queue'); msg_id | read_ct | enqueued_at | last_read_at | vt | message | headers
--------+---------+-----------------------------+-------------------------------+-------------------------------+-----------------+---------
1 | 1 | 2026-01-23 20:27:21.7741-06 | 2026-01-23 20:27:31.605236-06 | 2026-01-23 20:28:01.605236-06 | {"foo": "bar1"} |
Archiving a message removes it from the queue and inserts it to the archive table.
-- Archive message with msg_id=2.
SELECT pgmq.archive(
queue_name => 'my_queue',
msg_id => 2
); archive
--------------
t
(1 row)
Or archive several messages in one operation using msg_ids (plural) parameter:
First, send a batch of messages
SELECT pgmq.send_batch(
queue_name => 'my_queue',
msgs => ARRAY['{"foo": "bar3"}','{"foo": "bar4"}','{"foo": "bar5"}']::jsonb[]
); send_batch
------------
3
4
5
(3 rows)
Then archive them by using the msg_ids (plural) parameter.
SELECT pgmq.archive(
queue_name => 'my_queue',
msg_ids => ARRAY[3, 4, 5]
); archive
---------
3
4
5
(3 rows)
Archive tables can be inspected directly with SQL.
Archive tables have the prefix a_ in the pgmq schema.
SELECT * FROM pgmq.a_my_queue; msg_id | read_ct | enqueued_at | last_read_at | archived_at | vt | message | headers
--------+---------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+-----------------+---------
2 | 1 | 2026-01-23 20:32:35.291971-06 | 2026-01-23 20:32:42.938473-06 | 2026-01-23 20:33:20.297454-06 | 2026-01-23 20:33:12.938473-06 | {"foo": "bar2"} |
3 | 0 | 2026-01-23 20:33:25.414914-06 | | 2026-01-23 20:33:30.318465-06 | 2026-01-23 20:33:25.415035-06 | {"foo": "bar3"} |
4 | 0 | 2026-01-23 20:33:25.414914-06 | | 2026-01-23 20:33:30.318465-06 | 2026-01-23 20:33:25.415035-06 | {"foo": "bar4"} |
5 | 0 | 2026-01-23 20:33:25.414914-06 | | 2026-01-23 20:33:30.318465-06 | 2026-01-23 20:33:25.415035-06 | {"foo": "bar5"} |
Send another message, so that we can delete it.
SELECT pgmq.send('my_queue', '{"foo": "bar6"}'); send
-----------
6
(1 row)
Delete the message with id 6 from the queue named my_queue.
SELECT pgmq.delete('my_queue', 6); delete
-------------
t
(1 row)
Delete the queue my_queue.
SELECT pgmq.drop_queue('my_queue'); drop_queue
-----------------
t
(1 row)
pgmq guarantees exactly once delivery of a message within a visibility timeout. The visibility timeout is the amount of time a message is invisible to other consumers after it has been read by a consumer. If the message is NOT deleted or archived within the visibility timeout, it will become visible again and can be read by another consumer. The visibility timeout is set when a message is read from the queue, via pgmq.read(). It is recommended to set a vt value that is greater than the expected time it takes to process a message. After the application successfully processes the message, it should call pgmq.delete() to completely remove the message from the queue or pgmq.archive() to move it to the archive table for the queue.
As the pgmq community grows, we'd love to see who is using it. Please send a PR with your company name and @githubhandle.
Currently, officially using pgmq:
Thanks goes to these incredible people: