Skip to content

Quickstart

From nothing to one message pushed and consumed, with Docker, the JavaScript SDK, and plain curl.

Updated View as Markdown

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

  1. Create a Docker network so the broker can reach PostgreSQL by name.

    docker network create queen
  2. 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
  3. Start the broker. PG_HOST is the container name from the previous step. PG_USER and PG_DATABASE both default to postgres, so you only need the host, the port and the password. The broker listens on PORT, which defaults to 6632.

    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
  4. Check that it is up. /health is 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 with docker 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-mq

Point 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:

clients/client-js/test-v2/push.jsjs
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:

clients/client-js/test-v2/consume.jsjs
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:

clients/client-js/test-v2/pop.jsjs
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 Content

Two 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 returns status: "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=audit gets the same message again, from its own cursor. Groups are fan-out: each one sees everything, independently.

Clean up

docker rm -f queen queen-pg
docker network rm queen

Where to go next

Navigation

Type to search…

↑↓ navigate↵ selectEsc close