---
title: "Quickstart"
description: "From nothing to one message pushed and consumed, with Docker, the JavaScript SDK, and plain curl."
---

> Documentation Index
> Fetch the complete documentation index at: https://queenmq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

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.

```bash
   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.

```bash
   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`.

```bash
   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.

```bash
   curl -s http://localhost:6632/health
```

```json
   {"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.

> **Note**
>
> The broker also serves its dashboard: open `http://localhost:6632` in a
> browser. With auth off (the default, and this deployment) it boots straight
> into a standalone operator identity: no login, every view live, because the
> API underneath is already open. What each view shows, and what changes when
> JWT auth or the proxy enter the picture, is on
> [the dashboard page](/selfhost/dashboard).

## 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.

### JavaScript

Install the client:

```sh
npm install queen-mq
pnpm add queen-mq
yarn add queen-mq
bun add queen-mq
```

Point it at the broker. The constructor accepts a single URL string:

```js
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:

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:

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:

### curl

Push one message. `items` is an array; `queue` and `payload` are required, and
`partition` defaults to `Default` if you omit it:

```bash
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:

```json
[{"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:

```bash
curl -i "http://localhost:6632/api/v1/pop/queue/orders?batch=1&wait=true&consumerGroup=demo"
```

```json
{"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`:

```bash
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-..."}'
```

```json
[{"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:

```bash
curl -i "http://localhost:6632/api/v1/pop/queue/orders?batch=1&consumerGroup=demo"
```

```http
HTTP/1.1 204 No Content
```

> **Caution**
>
> **An empty pop is a bodiless 204.** Not an empty array, not `{"messages":[]}`.
> No body at all. This is deliberate: announcing a content-length on a body the
> server then elides made strict HTTP/1.1 clients treat the connection as poisoned
> and drop it, which under empty-poll load turned into connection-reset storms.
> Every `204` the broker returns is bodiless, so a client must branch on the status
> code before it tries to parse.

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

```bash
docker rm -f queen queen-pg
```

```bash
docker network rm queen
```

## Where to go next

- [Use Queen](/use) — The model in depth: partitions, consumer groups, acks as offset commits, leases, retries, transactions and replay.
- [Reference](/reference) — Every route, every environment variable, every metric, generated from the source.
- [Self-hosting](/selfhost) — What this quickstart skipped: TLS, authentication, PostgreSQL sizing, retention, probes and high availability.
- [Limits and non-goals](/start/limits) — Read this before you design around Queen, not after.

Source: https://queenmq.com/start/quickstart/index.mdx
