You need Docker and about five minutes. At the end you will have a PostgreSQL, a
broker, and one message that went in and came out: first through the JavaScript
SDK, then through curl, so you can see the wire.
The broker creates its own schema at boot: it embeds the DDL and the stored procedures in the binary and applies them under an advisory lock. There is no migration step to run, and no extensions to install into PostgreSQL.
Start a broker
-
Create a Docker network so the broker can reach PostgreSQL by name.
docker network create queen -
Start PostgreSQL. Any PostgreSQL the broker can reach works; the test harness runs
postgres:16. The password here has to match the one you give the broker in the next step.docker run -d --name queen-pg --network queen -e POSTGRES_PASSWORD=postgres postgres:16 -
Start the broker.
PG_HOSTis the container name from the previous step.PG_USERandPG_DATABASEboth default topostgres, so you only need the host, the port and the password. The broker listens onPORT, which defaults to6632.docker run -d --name queen --network queen -p 6632:6632 -e PG_HOST=queen-pg -e PG_PORT=5432 -e PG_PASSWORD=postgres ghcr.io/queen-mq/queen:latest -
Check that it is up.
/healthis not a static answer: it takes a connection from the pool and does a real database round-trip, so a healthy response means the broker and PostgreSQL are both working.curl -s http://localhost:6632/health{"status":"healthy","database":"connected","engine":"segments-rust","version":"1.0.0"}If PostgreSQL is unreachable you get HTTP 503 and
{"status":"unhealthy","database":"disconnected",…}instead. Check the broker’s logs withdocker logs queen. It prints the configuration it actually resolved at boot, including the PostgreSQL host it is dialling.
Push and consume
There is nothing to create first. A queue and a partition are provisioned by the first push that names them, so the only decision you make here is the partition key, the thing whose events must stay in order relative to each other.
Install the client:
npm i queen-mqyarn add queen-mqpnpm add queen-mqbun add queen-mqPoint it at the broker. The constructor accepts a single URL string:
import { Queen } from 'queen-mq'
const client = new Queen('http://localhost:6632')Push a message. data is your payload, and it must be JSON. The blocks below are
extracted verbatim from the files the JavaScript suite executes, which is why the
queue names look like test fixtures; substitute your own:
const res = await client
.queue('test-queue-v2')
.push([{ data: { message: 'Hello, world!' } }])The result is one entry per item, and status is queued when the message was
stored. Now consume it. .consume() polls, hands each message to your callback,
and acknowledges it when the callback resolves; .limit(1) makes it stop after
one message instead of running forever:
await client
.queue('test-queue-v2-consume')
.batch(1)
.limit(1)
.each()
.consume(async msg => {
msgToReturn = msg
})If you want the raw pop instead of the managed consumer loop, .pop() returns an
array of messages and leaves acknowledgement to you. .wait(true) turns on
long-polling, so the call parks until a message arrives or the timeout expires
rather than returning empty immediately:
const res = await client
.queue('test-queue-v2-pop-non-empty')
.batch(1)
.wait(true)
.pop()Push one message. items is an array; queue and payload are required, and
partition defaults to Default if you omit it:
curl -i -X POST http://localhost:6632/api/v1/push -H 'Content-Type: application/json' -d '{"items":[{"queue":"orders","partition":"customer-42","payload":{"hello":"world"}}]}'The response is HTTP 201 and a top-level array, one entry per item, in request order:
[{"index":0,"message_id":"019842f1-...","transaction_id":"019842f1-...","queueName":"orders","status":"queued"}]You did not supply a transactionId, so the broker minted one from the message
id. Supplying your own is what makes a retry of this call idempotent.
Now pop. wait=true long-polls instead of returning immediately, and
consumerGroup names the cursor that will remember what you consumed:
curl -i "http://localhost:6632/api/v1/pop/queue/orders?batch=1&wait=true&consumerGroup=demo"{"success":true,"queue":"orders","partition":"customer-42","partitionId":"6b1c...","leaseId":"019842f1-...","consumerGroup":"demo","messages":[{"id":"019842f1-...","transactionId":"019842f1-...","traceId":null,"data":{"hello":"world"},"producerSub":null,"createdAt":"2026-07-30T09:14:02.117364Z","partitionId":"6b1c...","partition":"customer-42","leaseId":"019842f1-...","consumerGroup":"demo"}]}That pop took a lease: the span is claimed for this consumer group until you
acknowledge it or the lease expires. Acknowledge it with the transactionId and
partitionId from the message, plus the leaseId:
curl -i -X POST http://localhost:6632/api/v1/ack -H 'Content-Type: application/json' -d '{"transactionId":"019842f1-...","partitionId":"6b1c...","status":"completed","consumerGroup":"demo","leaseId":"019842f1-..."}'[{"index":0,"transactionId":"019842f1-...","success":true,"error":null,"leaseReleased":true,"dlq":false,"noop":false}]The ack response is HTTP 200 even when an individual ack was rejected (a stale
lease, an offset already committed), so the per-item success flag is the signal
to check, not the status code.
What success looks like
Pop a queue you have already drained, with the same consumer group. There is nothing left, and this is what that looks like:
curl -i "http://localhost:6632/api/v1/pop/queue/orders?batch=1&consumerGroup=demo"HTTP/1.1 204 No ContentTwo more things worth doing before you move on, both of which show the model rather than the API:
- Push twice with the same
transactionId. The second push returnsstatus: "duplicate"and the first message’s id, having written nothing. Deduplication is exact, per partition, and bounded by a window that defaults to 3600 seconds. - Pop with a second consumer group.
consumerGroup=auditgets the same message again, from its own cursor. Groups are fan-out: each one sees everything, independently.
Clean up
docker rm -f queen queen-pgdocker network rm queenWhere to go next
Use Queen
The model in depth: partitions, consumer groups, acks as offset commits, leases, retries, transactions and replay.
Reference
Every route, every environment variable, every metric, generated from the source.
Self-hosting
What this quickstart skipped: TLS, authentication, PostgreSQL sizing, retention, probes and high availability.
Limits and non-goals
Read this before you design around Queen, not after.