One call writes one or more messages to one or more queues. Access level write-only, which
is the only route a write-only token can reach. The response is always a top-level array with
one element per request item, in request order.
Request
{
"items": [
{
"queue": "orders.created",
"partition": "customer-42",
"transactionId": "order-8891-created",
"payload": { "orderId": 8891, "total": 4200 }
}
]
}| Field | Type | Required | Default |
|---|---|---|---|
items |
array of objects | yes | n/a |
items[].queue |
string | yes | n/a |
items[].partition |
string | no | "Default" |
items[].payload |
any JSON value | yes | n/a |
items[].transactionId |
string | no | the message id the broker mints for this item |
Notes that change behaviour:
payloadis the wire field name. The JavaScript and Python clients also acceptdataon an item and rename it topayloadbefore sending; the Go client’s field isPayload. On raw HTTP,datais not recognised, and because unknown keys are ignored an item carryingdatabut nopayloadfails the body parse with a 400:payloadis required.payloadmay be any JSON value: object, array, string, number, boolean ornull. It is stored verbatim and spliced back into the pop response without re-serialization.traceIdis silently ignored on this route. The push path always stores a null trace id, so every message produced through/api/v1/pushcomes back from a pop withtraceId: null. A trace id can only be attached at write time throughPOST /api/v1/transaction.- Items in one call may target different queues and different partitions.
"items": []is accepted and returns201with an empty array.- When authentication is enabled, the validated token’s
subis stamped onto every frame and echoed by pops asproducerSub. AproducerSubsupplied in the request body is never honoured.
Queues and partitions are created by pushing
If the queue does not exist, the first push creates it, together with its configuration row and
its first partition. namespace and task are derived from a dotted name (orders.created
becomes namespace orders, task created), so a queue that was never explicitly configured is
still discoverable by a namespace or task pop. Queue options keep their defaults, which is where
the two-valued leaseTime comes from: a queue created implicitly by a push leases for 60
seconds, while a queue created or updated through /api/v1/configure leases for 300
seconds.
Partitions are created on first push to that partition name, and deleted automatically only
once they are empty and have been idle for PARTITION_CLEANUP_DAYS (30). A push to a name
whose partition was reclaimed simply creates it again, with a new id and offsets from 0.
Response
201 Created, Content-Type: application/json, a top-level array:
[
{
"index": 0,
"message_id": "0198f2c1-4d3a-7c10-9f2b-6a1e5d0c7b83",
"transaction_id": "order-8891-created",
"queueName": "orders.created",
"status": "queued"
}
]| Field | Type | Meaning |
|---|---|---|
index |
integer | the item’s position in the request array |
message_id |
string (UUIDv7) | the stored message’s id; for a duplicate, the id of the original message |
transaction_id |
string | the deduplication key actually used: your transactionId, or the minted message id when you omitted it |
queueName |
string | the queue the item targeted |
status |
string | one of the four values below |
The mixed casing (message_id and transaction_id snake_case, queueName camelCase) is the
wire contract, not a typo, and clients depend on it.
There is no partition, no partitionId, no offset and no createdAt in a push result. The
offset a message landed on is never returned to a producer; positions are internal to the
storage engine and are only ever used through cursors.
Item statuses
status |
What happened | What to do |
|---|---|---|
queued |
stored and committed | nothing |
duplicate |
an earlier message with the same transactionId already exists in this partition, inside the deduplication window; nothing was written |
nothing. This is the idempotent outcome |
buffered |
the message went to the broker’s on-disk spool instead of PostgreSQL, and a background drain will replay it: either because the database transaction failed, or because the broker is in maintenance mode, which diverts every push to the spool | treat as accepted; if maintenance mode is off, investigate the database |
failed |
the write did not reach PostgreSQL and the spool write also failed; the message exists nowhere | retry it |
Replay from the spool preserves the transactionId, so deduplication makes it idempotent: a
message that was both buffered and (by your own retry) pushed again lands once.
Status codes
| Code | When |
|---|---|
201 |
the normal path, whatever the per-item statuses; also the maintenance-mode path when every item spooled |
400 |
the body is not JSON, or items / queue / payload is missing: {"error":"bad body: …"} |
403 |
authentication is on and the token has no writer role |
413 |
the body exceeds 64 MiB |
500 |
maintenance mode is on and at least one spool write failed |
Atomicity
Items are grouped by (queue, partition), and each group becomes one segment. The commit
boundary is the group, not the request. A push spanning three partitions can have one group
committed and another failed, which is exactly what the per-item statuses report. If you need
several writes to land together, use
POST /api/v1/transaction, which runs the whole bundle in one
PostgreSQL transaction.
The handler does not answer until every group has resolved, so the call’s latency includes the broker’s write-coalescing hold window: concurrent pushes to different partitions are bundled into a single database transaction, which is what makes one commit carry many messages.
Deduplication
Deduplication is on by default, exact, and scoped to one partition. The key is the item’s
transactionId; the bound is the queue’s dedupWindowSeconds, whose default is 3600.
Setting it to 0 disables deduplication for that queue.
Two layers run, in this order:
- Within one request. The first item for a given
(queue, partition, transactionId)wins. Later items in the same body produce no frame and come backduplicate, carrying the winner’smessage_id. - In the database. Before allocating an offset, the push probes the partition’s transaction
index for the incoming hashes while holding the partition’s row lock. A hit returns
duplicatehaving written nothing at all: there is no insert to roll back, and no subtransaction. A clean probe proceeds to allocate.
Because the probe happens under the same row lock that serializes writes to the partition, two
concurrent pushes of the same transactionId cannot both succeed.
const res1 = await client
.queue('test-queue-v2')
.push([{ transactionId: 'test-transaction-id', data: { message: 'Hello, world!' } }])
const res2 = await client
.queue('test-queue-v2')
.push([{ transactionId: 'test-transaction-id', data: { message: 'Hello, world!' } }])
// res1[0].status === 'queued', res2[0].status === 'duplicate'What you get back for a duplicate is the original message’s id, resolved from the stored
segment. If the original can no longer be read (retention removed its segment, for instance),
the id comes back as the all-zeros UUID 00000000-0000-0000-0000-000000000000, the
“original unknown” sentinel. The status is still duplicate.
Examples
const res = await client
.queue('test-queue-v2')
.push([{ data: { message: 'Hello, world!' } }])res = await client.queue("test-queue-v2").push([{"data": {"message": "Hello, world!"}}])responses, err := client.Queue(queueName).Push(payload).Execute(ctx)
if err != nil {
t.Fatalf("Failed to push message: %v", err)
}Raw HTTP, with an explicit deduplication key:
curl -sS -X POST http://localhost:6632/api/v1/push -H 'content-type: application/json' -d '{"items":[{"queue":"orders.created","partition":"customer-42","transactionId":"order-8891-created","payload":{"orderId":8891}}]}'Encryption at rest
If the queue has encryptionEnabled and the broker has a valid QUEEN_ENCRYPTION_KEY, payloads
are encrypted before they are packed into a segment, and pops decrypt them transparently. A
malformed or wrong-length key disables encryption and stores plaintext with only a warning
in the log. It is not a boot failure and not a push failure. Self-hosting covers
key handling.