Consumption state on this engine is one cursor per (partition, consumer group):
queen.log_consumers.committed, the highest offset that group has completed in
that partition. There is no per-message state, so every number on these routes is
arithmetic over cursors and partition watermarks rather than a scan.
A pop that carries no consumerGroup uses the reserved group name
__QUEUE_MODE__, which is why that name shows up in listings on queues where
nobody set a group explicitly. Groups are created implicitly on first contact:
the first pop for a (queue, group) writes a marker row in
queen.consumer_groups_metadata and seeds a cursor for every existing partition
in one statement.
Reads are read-only. subscription and both seek routes are read-write.
Both DELETE routes are admin: route_access_level maps every DELETE
under /api/v1/consumer-groups/ to admin regardless of what follows.
GET /api/v1/consumer-groups
Every group the tenant has, one entry per (group, queue) pair. The body is a bare JSON array.
[
{
"name": "invoicer",
"topics": ["orders"],
"queueName": "orders",
"members": 8,
"partitionCursors": 8,
"partitionsWithLag": 2,
"totalLag": 314,
"maxTimeLag": 42,
"state": "Stable",
"storage": "segments",
"subscriptionMode": "new",
"subscriptionTimestamp": "2026-07-30T08:00:00Z",
"subscriptionCreatedAt": "2026-07-30T08:00:00Z",
"conflation": false
}
]Read these fields carefully:
memberscounts cursor rows, not consumer processes. One consumer on a 32-partition queue reportsmembers: 32.partitionCursorscarries the same number under its true name;membersis kept only for wire compatibility.totalLagis the sum over partitions ofGREATEST(last_offset - GREATEST(committed, log_start - 1), 0): pure arithmetic, exact, and no segment scan. Frames already evicted by retention are not counted as lag.maxTimeLagis the age in seconds of the oldest unconsumed segment. This is the one value that touchesqueen.log_segments.stateis derived, not stored:LaggingwhenmaxTimeLag > 300, otherwiseStablewhen the group has ever consumed anything, otherwiseDead.subscriptionMode,subscriptionTimestampandsubscriptionCreatedAtare the group’s durable registration on this queue, read fromqueen.consumer_groups_metadata. Before 1.1.0 all three were hard-codednull, which is why a group’s start position had no console surface at all. They are stillnullfor a group that has cursors but no registration row, i.e. a group that only ever used the pinned single-partition route without declaring a delivery policy, which is the one path that does not register.conflation(1.1.0) is the group’s stored delivery policy on this queue:truemeans last-value delivery, wheretotalLagandpartitionsWithLagmean different things. For a conflating grouptotalLagis log lag, the positions still to retire, most of which will be skipped;partitionsWithLagis the number of handler runs actually outstanding, and it is the figure to alert on.
GET /api/v1/consumer-groups/lagging
Query: minLagSeconds (default 3600). Returns a bare array of
(group, partition) pairs whose time lag exceeds the threshold. The static
lagging path is registered before the :group parameter route, so it cannot be
shadowed by a group actually named lagging.
[
{
"consumer_group": "invoicer",
"queue_name": "orders",
"partition_name": "eu-3",
"partition_id": "0198...",
"worker_id": "0198...",
"offset_lag": 512,
"time_lag_seconds": 7421,
"lag_hours": 2.06,
"oldest_unconsumed_at": "2026-07-30T07:41:02.010Z",
"last_consumed_at": null
}
]Keys are snake_case here: this route predates the camelCase listings and its
shape is unchanged. time_lag_seconds is derived from the oldest unconsumed
segment’s created_at, so a partition whose entire backlog was evicted drops out
of the result rather than reporting a huge lag. last_consumed_at is always
null: queen.log_consumers does not carry that column.
GET /api/v1/consumer-groups/:group
One group, broken out per queue and partition. The body is an object keyed by queue name.
{
"orders": {
"conflation": false,
"partitions": [
{
"partition": "eu-1",
"workerId": "0198...",
"lastConsumedAt": null,
"totalConsumed": 91204,
"offsetLag": 0,
"timeLagSeconds": 0,
"leaseActive": false
}
]
}
}leaseActive is true only while lease_expires_at is in the future. That is the
one in-flight leased batch this group holds on the partition. totalConsumed is
the group’s lifetime counter from queen.log_consumers.total_consumed, and it
counts log positions retired, not handler runs: under
conflation it keeps counting the skipped
positions too, which is what the lag and state derivations want. conflation
(1.1.0) is the group’s stored delivery policy on that queue. A group with no
cursors returns {}.
POST /api/v1/consumer-groups/:group/subscription
Body: subscriptionTimestamp (required, non-empty). A missing or empty value is a
400.
{ "subscriptionTimestamp": "2026-07-30T08:00:00Z" }Response:
{
"success": true,
"consumerGroup": "invoicer",
"newTimestamp": "2026-07-30T08:00:00Z",
"rowsUpdated": 3
}What this actually changes: the stored subscription timestamp on the group’s
queen.consumer_groups_metadata rows (for every queue the group has touched,
because the update carries no queue predicate), and it deletes the group’s
empty-scan watermarks so a backward move re-exposes cold partitions to the next
pop’s candidate scan.
Seek
Two routes, one body shape. Send either toEnd or timestamp:
{ "toEnd": true }{ "timestamp": "2026-07-30T08:00:00Z" }A body with neither is a 400 (Must specify toEnd=true or a timestamp). The
per-partition route additionally accepts an empty body, which it treats as
toEnd: true. That is what the dashboard’s per-partition “skip to end” button
sends.
Cursor placement:
toEndsetscommitted = last_offset, so the next wanted offset islast_offset + 1and the whole existing backlog is skipped.timestampsetscommittedto the base offset of the first segment withcreated_at >= timestamp, minus one. Resolution is therefore segment-granular, not per-message. If no segment is that recent, the cursor lands onlast_offset(a future-only cursor).
Either way the seek releases any live lease on the partition and resets the retry/redelivery state for that (partition, group). Because the timestamp walk starts at the retention watermark, seeking to a timestamp older than what is retained lands on the oldest retained segment.
POST /api/v1/consumer-groups/:group/queues/:queue/seek
Seeks every partition of the queue.
{
"success": true,
"consumerGroup": "invoicer",
"queueName": "orders",
"seekToEnd": true,
"targetTimestamp": null,
"partitionsUpdated": 8
}If no partition matched (the queue does not exist for this tenant, or has no partitions yet), the stored procedure fails loudly rather than reporting a successful no-op:
{
"success": false,
"error": "Queue not found or has no partitions",
"consumerGroup": "invoicer",
"queueName": "nope",
"partitionsUpdated": 0
}That body is served with HTTP 404: the broker maps a stored-procedure error
containing “not found” to 404 and anything else to 500.
POST /api/v1/consumer-groups/:group/queues/:queue/partitions/:partition/seek
Seeks one named partition.
{
"success": true,
"consumerGroup": "invoicer",
"queueName": "orders",
"partitionName": "eu-1",
"seekToEnd": false,
"targetTimestamp": "2026-07-30T08:00:00.000Z",
"updated": true
}An unknown partition returns {"success": false, "error": "Partition not found"}
at 404.
After a successful seek the broker repairs its in-memory discovery ring for
(queue, group) and wakes parked pops, so a backward seek starts redelivering
immediately instead of waiting for the periodic reseed. A queue-wide seek
re-derives the ring from committed database state, which only re-adds partitions
whose last_offset is genuinely ahead of committed, so a seek to end adds
nothing; a single-partition seek marks that one partition. The other brokers are
told over the mesh, and the seek also writes a durable repair marker they read on
their reconcile pass, so a peer that missed the frame repairs within one interval
rather than at its next full reseed. See the hot-list.
If that repair fails on this broker, the cursor still moved, so the response is
still 200 and still "success": true, with two extra keys:
{
"hotlistRepaired": false,
"warning": "the cursor moved, but re-discovering the partitions it made pending failed on this broker; delivery of the replayed messages resumes at the next full reseed"
}A seek whose repair worked is byte-identical to what it has always been: neither key appears.
DELETE routes
Both are admin. Both accept deleteMetadata (default true) and both answer
200 with a JSON body, not 204, because the SDKs read fields off the response.
DELETE /api/v1/consumer-groups/:group
Removes the group’s cursors across every queue of the tenant, plus its
empty-scan watermarks, plus (when deleteMetadata) its
queen.consumer_groups_metadata rows.
{
"success": true,
"consumerGroup": "invoicer",
"deletedPartitions": 24,
"metadataDeleted": true
}deletedPartitions is the number of queen.log_consumers cursor rows removed.
DELETE /api/v1/consumer-groups/:group/queues/:queue
The same operation scoped to one queue: cursors for that queue’s partitions and
the (queue, group) watermark row.
{
"success": true,
"consumerGroup": "invoicer",
"queueName": "orders",
"deletedPartitions": 8,
"metadataDeleted": true
}Clearing the watermark is the part that matters: an advanced empty-scan watermark would otherwise fence off every partition and the recreated group would appear to consume nothing.
Like a seek, a delete is a cursor move that writes nothing, so the other brokers cannot discover it on their own: they still hold a ring for the group name and would keep serving from it. The broker that runs the delete therefore repairs the queues it holds a ring for and hands the result to its peers, and the delete also publishes a durable repair marker for the peers whose queues this broker does not poll. Before the reseed was windowed this healed within one 30-second floor because every broker walked every partition that often.
Access levels
| Route | Level |
|---|---|
GET /api/v1/consumer-groups |
read-only |
GET /api/v1/consumer-groups/lagging |
read-only |
GET /api/v1/consumer-groups/:group |
read-only |
POST /api/v1/consumer-groups/:group/subscription |
read-write |
POST /api/v1/consumer-groups/:group/queues/:queue/seek |
read-write |
POST /api/v1/consumer-groups/:group/queues/:queue/partitions/:partition/seek |
read-write |
DELETE /api/v1/consumer-groups/:group |
admin |
DELETE /api/v1/consumer-groups/:group/queues/:queue |
admin |
Every route here is tenant-scoped: the group name travels into the stored
procedure together with the tenant, so two tenants can run a group called
workers on their own queues without seeing each other’s cursors. Related pages:
queues and resources,
messages and DLQ.