A trace is an application-written event attached to a message address. The broker
stores what you post and gives you three ways to read it back; it never writes
traces itself and never interprets the data you send. Use them to answer “what
happened to this message, in the words of the workers that handled it”
(retry reasons, external system responses, business checkpoints) for messages whose
payload alone does not explain their fate.
Traces are engine-agnostic: they live in queen.message_traces, keyed by
(partition_id, transaction_id), with optional labels in
queen.message_trace_names. That key is why the read routes take the same
two-segment address as the message routes.
POST /api/v1/traces
Access level read-write. Required: transactionId, partitionId, data. A
missing or empty transactionId/partitionId is a 400
(transactionId and partitionId are required); a missing data is a 400
(data is required).
{
"transactionId": "order-4471",
"partitionId": "0198e0f2-1c44-7b91-9c31-9a4b8f2ad001",
"consumerGroup": "invoicer",
"eventType": "retry",
"data": { "reason": "downstream 503", "attempt": 2 },
"traceNames": ["payment-failures", "order-4471"]
}| Field | Default | Notes |
|---|---|---|
transactionId |
required | Free text; not validated against any message. |
partitionId |
required | Must be a partition UUID. |
consumerGroup |
__QUEUE_MODE__ |
The group the event is attributed to. |
eventType |
"info" |
Free text. |
data |
required | Any JSON value; stored as given. |
traceNames |
none | Array of labels, or a single string, which is wrapped into a one-element array. Anything else is ignored. |
workerId is always stored as seg-rust. The handler stamps it and does not
read a client-supplied value.
Success is 201 with {"success": true, "traceId": "0198..."}. A stored-procedure
body carrying success: false or a non-null error is served as 500.
With QUEEN_TENANCY_HEADER enabled the partitionId is checked against the
caller’s tenant before anything is written, because it arrives straight off the
wire and a leaked or guessed UUID would otherwise let anyone attach records to
another tenant’s messages. A foreign partition gets the same
{"success": false, "error": "Message not found"} at 404 that an unknown one
would. With tenancy off there is no gate, and a partitionId that is not a valid
UUID reaches the stored procedure and comes back as 500
({"error":"trace failed: ..."}).
GET /api/v1/traces/:partitionId/:transactionId
All traces for one message address, oldest first.
{
"traces": [
{
"id": "0198...",
"event_type": "retry",
"data": { "reason": "downstream 503", "attempt": 2 },
"consumer_group": "invoicer",
"worker_id": "seg-rust",
"created_at": "2026-07-30T10:52:11.400Z",
"trace_names": ["payment-failures", "order-4471"]
}
]
}Keys are snake_case on all three read routes. An address with no traces returns
{"traces": []} at 200, and so does a partition belonging to another tenant,
which is the same answer for the same reason as the message routes: an empty set
leaks nothing about whether the partition exists elsewhere.
GET /api/v1/traces/by-name/:traceName
Every trace carrying one label, oldest first, with the queue and partition names
resolved through queen.log_partitions and queen.queues.
Query: limit (default 100), offset (default 0).
{
"traces": [
{
"id": "0198...",
"transaction_id": "order-4471",
"partition_id": "0198...",
"event_type": "retry",
"data": { "reason": "downstream 503", "attempt": 2 },
"consumer_group": "invoicer",
"worker_id": "seg-rust",
"created_at": "2026-07-30T10:52:11.400Z",
"queue_name": "orders",
"partition_name": "eu-1",
"message_payload": null,
"trace_names": ["payment-failures", "order-4471"]
}
],
"total": 1,
"pagination": { "limit": 100, "offset": 0 }
}message_payload is null by construction: payloads live inside compressed
segment blobs that SQL cannot open, and the key is kept only so the JSON shape
does not change. Use GET /api/v1/messages/:partitionId/:transactionId to fetch
the payload. See messages and DLQ.
This route is name-addressed, so there is no partition id to gate on. Ownership is
resolved per trace through its partition (queen.log_partitions.queue_id is the
queen.queues id), and only traces belonging to the caller’s tenant are counted
and returned. total here is a real count of the tenant-scoped match set, not
the page length.
GET /api/v1/traces/names
The index of labels that exist, most recently seen first.
Query: limit (default 50), offset (default 0).
{
"trace_names": [
{
"trace_name": "payment-failures",
"trace_count": 214,
"message_count": 173,
"last_seen": "2026-07-30T10:52:11.400Z"
}
],
"total": 4,
"pagination": { "limit": 50, "offset": 0 }
}trace_count counts trace rows, message_count counts distinct transaction ids.
A name appears only when at least one of its traces belongs to the caller’s tenant,
and its counts and last_seen are computed from that tenant’s traces alone:
trace names are caller-chosen text, so a global index would leak both the names
and the activity times of every other tenant.
Operational limits
Two properties of the trace tables that will surprise you eventually:
- Nothing purges traces. Retention, the DLQ purge and the metrics cleanup all
leave
queen.message_tracesandqueen.message_trace_namesalone. Trace volume is entirely your writes; size the table accordingly, and prune it yourself if you trace at message rate. - Deleting a queue leaves its traces behind. The foreign keys from
queen.message_tracesto the message and partition tables were dropped so the table could serve both engines, so a queue delete does not cascade. The orphaned rows stay in the table but become unattributable: the by-name and names routes resolve ownership through the partition, and a trace whose partition row is gone belongs to nobody, so it is filtered out rather than shown to everyone. It is still readable by exact address.
Access levels
| Route | Level |
|---|---|
POST /api/v1/traces |
read-write |
GET /api/v1/traces/:partitionId/:transactionId |
read-only |
GET /api/v1/traces/by-name/:traceName |
read-only |
GET /api/v1/traces/names |
read-only |
All four are tenant-scoped. Note the routing shape: /api/v1/traces/names is one
segment deep and cannot collide with the two-segment address route, but
/api/v1/traces/by-name/:traceName and
/api/v1/traces/:partitionId/:transactionId are both two segments deep and the
static by-name wins. A partition whose id were literally by-name is
unreachable. Partition ids are UUIDs, so this is a routing fact rather than a
practical limit.