A queue does not have to be created before it is used: the first push to a queue
name creates the queue, one partition, and a queen.queues configuration row
with column defaults. It also derives namespace and task from a dotted queue
name (billing.invoice becomes namespace billing, task invoice), so a
push-only queue is still visible to namespace and task discovery pops.
POST /api/v1/configure exists to set options that differ from the defaults, and to
pre-create a queue so a consumer can attach before any producer runs.
A pop can create the row too: a queue-scoped pop on a name that does not
exist creates the queen.queues row with column defaults, because a consumer
group may legitimately subscribe before the first push and its durable
subscription record hangs off the queue’s id. The practical consequence is that
a consumer-side typo now creates an empty queue rather than nothing.
Two routes mutate queue state: POST /api/v1/configure (read-write) and
DELETE /api/v1/resources/queues/:queue (admin). Everything else on this page
is a read at the read-only access level.
POST /api/v1/configure
queue is the only required field. Options may be nested under options or sent
at the top level; the broker normalises them into one bag before calling
queen.configure_queue_v1. Top-level namespace and task are folded into the
options when they are non-empty strings.
{
"queue": "orders",
"namespace": "billing",
"task": "invoice",
"options": {
"leaseTime": 120,
"retryLimit": 5,
"dedupWindowSeconds": 600
}
}An empty string is a valid queue name: queen.configure_queue_v1 creates a row
named '', and the JavaScript client’s own load test relies on it. A missing or
non-string queue is a 400.
Configure is a full replace
Every call rewrites the whole configuration row. Keys you omit are reset to their defaults, not left as they were.
The reset is structural: configure_queue_v1 parses each option with
COALESCE(p_options->>'key', <default>) and writes every column on
ON CONFLICT DO UPDATE, so an absent key becomes the default value rather than
being skipped.
Enforced options
These are the options this engine reads. Each row names what enforces it, so you can predict the behaviour change from setting it.
| Option | Default | What enforces it |
|---|---|---|
namespace |
"" |
Stored on queen.queues; selects queues for the discovery pop and for namespace filters on the listings and analytics routes. |
task |
"" |
Same as namespace, on the task axis. |
leaseTime |
300 (seconds) |
Written to queen.queues.lease_time; the pop path uses COALESCE(request leaseSeconds, queue lease_time, 60) for the lease it takes. |
retryLimit |
3 |
The per-(partition, group) retry budget charged by an explicit failed ack (005_log_ack.sql). Below the limit the batch redelivers; at the limit the head frame is dead-lettered or dropped. |
deadLetterQueue |
true |
With dlqAfterMaxRetries, decides whether an exhausted retry budget writes queen.log_dlq. |
dlqAfterMaxRetries |
true |
See the note below: the two flags are OR-ed. |
delayedProcessing |
0 (seconds) |
Pop visibility: only segments at least this old are delivered. In-flight backlog is bounded by rate × delay. |
windowBuffer |
0 (seconds) |
Pop visibility: if the partition received a segment within this window, the pop delivers nothing from that partition. Trades latency for larger batches. |
minPopWaitTime |
0 (ms, off) |
How long a non-empty pop may hold an under-full batch before claiming it, so one commit carries more messages. Clamped by SQL to [0, 60000]. |
retentionEnabled |
false |
Master switch for the two age-based retention rules. Without it, retentionSeconds and completedRetentionSeconds do nothing. |
retentionSeconds |
0 |
Rule 1 cutoff. Active only when retentionEnabled is true and the value is positive. Whole segments older than the cutoff are deleted. |
completedRetentionSeconds |
0 |
Rule 2 cutoff: segments every group has consumed past. Also widens the queen.log_txns purge window (GREATEST(dedupWindowSeconds, completedRetentionSeconds, 900)). |
maxWaitTimeSeconds |
0 |
Deletes whole segments older than the cutoff for every consumer group, in-flight leases included. Independent of retentionEnabled. See below. |
encryptionEnabled |
false |
Stored on queen.queues and read at push time; payloads are stored as an encrypted envelope when a key is configured. |
dedupWindowSeconds |
3600 (seconds) |
Written to queen.queues.dedup_window_seconds. The push path probes queen.log_txns within this window before allocating an offset. 0 disables the probe entirely. |
Values are stored as given. The broker does not validate ranges, apart from the
SQL clamp on minPopWaitTime and a max(0) floor on dedupWindowSeconds.
Disabling the dead-letter queue takes both flags
005_log_ack.sql computes the DLQ switch as
COALESCE(dead_letter_queue, true) OR COALESCE(dlq_after_max_retries, true).
Setting only one of them to false leaves dead-lettering on. Send both as
false to turn it off, at which point an exhausted retry budget drops the
poison frame instead of quarantining it.
maxWaitTimeSeconds deletes messages
maxWaitTimeSeconds is not a delivery deadline and it does not dead-letter.
queen.log_evict_max_wait_step_v1 delegates to the same delete-and-advance path
retention uses, with the cutoff taken from queen.queues.max_wait_time_seconds:
whole segments older than the cutoff are removed for every consumer group,
including groups holding a live lease on them. The SQL calls this data loss by
design. A cursor left below the new log start resumes at the next existing
offset; the pop scan tolerates the gap. Use it only where dropping stale work is
the intent.
Response
200 with the stored-procedure JSON passed through verbatim:
{
"configured": true,
"queueId": "0198...",
"partitionId": null,
"queue": "orders",
"namespace": "billing",
"task": "invoice",
"storage": "segments",
"options": { "leaseTime": 120, "retryLimit": 5, "dedupWindowSeconds": 600 }
}storage is a hard-coded literal. There is no engine-selector column behind it
(there is only one engine); the key is kept so the response shape does not change.
partitionId is always null. Configuring a queue does not create a partition:
partitions are created by the first push that names one. The key used to carry the id of
a Default partition the engine never used, and it was removed on 2026-07-30 along with
the rows-engine table that held it. The key itself is kept so the response shape does not
change.
The options object echoes the resolved configuration: the values actually
stored, after defaulting, dedupWindowSeconds included. Clients assert on
configured === true and round-trip the option keys, so the body is never
reshaped by the broker.
queen.configure_queue_v1 owns every configuration write: one row,
queen.queues, carries the whole configuration including lease_time and
dedup_window_seconds. After the stored procedure returns, the handler does one
more thing: it drops this queue’s cached lease time (locally, and on mesh peers
via a QUEUE_CONFIG_SET frame), so a leaseTime change is visible on the next
pop. If the stored procedure returns a body carrying an error key, the handler
short-circuits before that invalidation.
Failures are 500 with {"error":"configure failed: ..."}.
GET /api/v1/resources/queues
Lists the tenant’s queues.
{
"queues": [
{
"id": "0198...",
"name": "orders",
"namespace": "billing",
"task": "invoice",
"createdAt": "2026-07-30T09:12:44.011Z",
"partitions": 8,
"retainedBytes": 194883,
"segments": { "segments": 41, "messages": 12904 },
"messages": { "total": 12904, "pending": 310, "processing": 24 }
}
]
}Two different clocks feed one object, and the difference matters when you read these numbers:
partitions,segmentsandmessages.totalare computed live per request fromqueen.log_partitions/queen.log_segments(log_queue_stats_all_v1).messages.totalis the count of retained frames, not the backlog.messages.pendingandmessages.processingcome fromqueen.stats, which a background reconciler refreshes everySTATS_INTERVAL_MS(default10000ms) on whichever replica wins advisory lock737002. They are the same watermark values the overview sums, so the list agrees with the rest of the dashboard rather than with a fresher count.
GET /api/v1/resources/queues/:queue
One queue in detail: identity, per-partition stats, and totals.
{
"id": "0198...",
"name": "orders",
"namespace": "billing",
"task": "invoice",
"createdAt": "2026-07-30T09:12:44.011Z",
"partitions": [
{
"id": "0198...",
"name": "Default",
"createdAt": "2026-07-30T09:12:44.031Z",
"stats": {
"total": 1600, "pending": 40, "processing": 8,
"completed": 1552, "failed": null, "deadLetter": 0
},
"oldestMessage": "2026-07-30T09:12:45.100Z",
"newestMessage": "2026-07-30T10:44:02.900Z"
}
],
"totals": { "total": 12904, "pending": 310, "processing": 24, "completed": 12570, "failed": null, "deadLetter": 0 },
"retainedBytes": 194883,
"segments": { "segments": 41, "messages": 12904 }
}Notes that follow from the log engine: pending is measured against the worst
(lowest) committed cursor across the queue’s consumer groups, so a single lagging
group holds the number up; failed is always null because there is no
per-message failure state; and retainedBytes is a queen.stats value, refreshed
on the stats cadence. A queue with no queen.queues row answers 404 with
{"error":"Queue not found"}.
DELETE /api/v1/resources/queues/:queue
Access level admin. One call to queen.delete_queue_v1
(013_analytics) owns the whole delete. It removes, in order, the queue’s
queen.log_txns and queen.log_dlq rows (neither carries a foreign key by
design, because the purge path must never pay FK-trigger cost, so they are
deleted explicitly while the partitions still resolve), then the single queen.queues
row. That one delete cascades to everything else: queen.log_partitions and,
under it, log_segments and log_consumers, plus the consumer watermarks,
queue-scoped consumer-group metadata, lag metrics and queen.stats rows.
Before the queue-identity merge this was two non-atomic steps (a stored
procedure plus a broker-side teardown of the engine tables); it is now one
transaction.
The response is 200 with a JSON body, never 204: the SDKs read
res.deleted === true and a bodyless response would make them see null.
{ "deleted": true, "queue": "orders", "existed": true }Deleting a queue that does not exist is also 200, with the body made
self-consistent rather than misleading:
{
"deleted": false,
"queue": "nope",
"existed": false,
"message": "Queue not found, nothing was deleted"
}The status stays 200 because the SDKs use delete-before-create as a cleanup
idiom; a 404 would turn a no-op into a thrown error for them. Check deleted,
not the status code.
Message traces are not deleted. queen.message_traces had its foreign keys
dropped so it could hold traces for both engines, so a queue delete leaves its
traces behind. See Traces.
Aggregate listings
Three sibling reads, all read-only, all passing their stored procedure’s JSON
through unchanged.
| Route | Returns |
|---|---|
GET /api/v1/resources/overview |
Counts (queues, partitions, namespaces, tasks), a messages block (total, pending, processing, completed, failed, deadLetter), a lag block (time and offset, avg/median/min/max), a throughput block (ingestedPerSecond, processedPerSecond over the last 5 minutes ÷ 300), timestamp and statsAge. |
GET /api/v1/resources/namespaces |
namespaces[] with namespace, queues, partitions, messages.total, messages.pending. |
GET /api/v1/resources/tasks |
The same shape on the task axis. |
statsAge is the age in seconds of the numbers you are reading, or -1 when
nothing has been reconciled yet. Treat a large statsAge as “the reconciler is
not running”, not as “the queue is idle”.
Access levels
Access levels are a role set, not a ladder. A WriteOnly token passes
/api/v1/push and is rejected on every route on this page, including the
listings.
| Route | Level |
|---|---|
POST /api/v1/configure |
read-write |
GET /api/v1/resources/queues |
read-only |
GET /api/v1/resources/queues/:queue |
read-only |
DELETE /api/v1/resources/queues/:queue |
admin |
GET /api/v1/resources/overview |
read-only |
GET /api/v1/resources/namespaces |
read-only |
GET /api/v1/resources/tasks |
read-only |
Every route here resolves a tenant from the request, so with
QUEEN_TENANCY_HEADER on they are scoped to the caller’s tenant. The full route
table, generated from the router and the authorization function, is in the
reference index.