Skip to content

Prometheus metrics

What GET /metrics/prometheus exposes, how process and cluster series differ, and why the endpoint must never be handed to a tenant.

Updated View as Markdown

GET /metrics/prometheus returns exposition text with content type text/plain; version=0.0.4; charset=utf-8 and Cache-Control: no-cache. Every family carries # HELP and # TYPE.

GET /metrics is a different endpoint and is not Prometheus text: it returns a small JSON object with uptime, aggregate request and message counts, pool size and RSS. Scraping it will fail to parse.

The endpoint cannot be protected by broker authentication

/health, /metrics and /metrics/prometheus resolve to access level public in the route table, and the authentication middleware short-circuits on public before it ever looks for a token. They are also in the default JWT_SKIP_PATHS. Removing them from that list changes nothing, because the level check wins.

Process versus cluster

Two namespaces answer two different questions, and mixing them is the most common mistake in a Queen dashboard.

queen_process_* counts what this one broker instance did since it started. The counters live in memory, so they reset to zero on every restart, and each instance behind a load balancer reports its own numbers. Use them for rate queries and for attributing load to an instance.

queen_cluster_* are lifetime totals read back out of PostgreSQL, from queen.worker_metrics_summary, which the metrics collector maintains. They survive restarts, and every instance in the cluster reports the same value because they all read the same row. Summing them across instances multiplies your traffic by the number of brokers. Aggregate them with max, never sum.

The in-process families beyond queen_process_* (pool gauges, spool gauges, the maintenance flag, the Vegas limits, the fusion batch families, parked long-polls) are also per-instance and also reset on restart.

How a scrape is assembled

The response is built in one pass and in a fixed order:

  1. In-process gauges and counters, always emitted.
  2. queen_seg_push_vegas_limit and queen_seg_pop_vegas_limit, the adaptive concurrency limits.
  3. Pool gauges (queen_db_pool_size, _idle, _active), the push maintenance-mode flag, and the disk spool gauges.
  4. One call to the database aggregator, whose JSON becomes the queen_cluster_* families, the per-queue minute rates, and the DLQ depths.

Step 4 is best-effort. If the pool has no connection or the read fails, the DB-backed families are silently absent from that scrape and the in-process block is still returned with HTTP 200. An alert on the absence of queen_cluster_* therefore detects a database problem; an alert on non-200 does not.

Because step 4 is a database round trip per scrape, the scrape interval is a load knob. Its per-queue and per-worker reads are bounded to buckets from the last five minutes; the lifetime totals are a single-row read and the dead-letter depths are counts.

Label sets

Family or group Labels
queen_cluster_*_total scope="cluster"
queen_cluster_ack_total scope="cluster", result = success or failed
queen_batches_fired_total, queen_batch_items_fired_total, queen_fusion_items_per_batch op = push, pop or ack
queen_batch_rtt_milliseconds op, plus quantile = 0.5 or 0.99
queen_queue_*_per_minute, queen_queue_parked_consumers, queen_queue_metrics_age_seconds queue
queen_queue_pop_lag_milliseconds queue, stat = avg or max
queen_queue_ack_per_minute queue, result = success or failed
queen_dlq_depth scope="cluster"
queen_dlq_depth_by_queue, queen_parked_long_polls queue
everything else none

queen_batch_rtt_milliseconds is a gauge that happens to carry a quantile label. It is not a Prometheus summary, and quantile here is a plain label computed from a bounded in-process ring of recent round trips. Queue names are escaped for backslash, double quote and newline.

Freshness of the per-queue families

Per-queue series do not come from live counters. Each instance flushes its per-queue deltas into minute buckets in PostgreSQL every METRICS_FLUSH_MS (60 s by default), and the aggregator reads the most recent bucket per queue within a five-minute window. Consequences:

  • a per-queue value can be up to one flush interval stale;
  • queen_queue_metrics_age_seconds is the age of the bucket the value came from (use it to distinguish “zero traffic” from “stale bucket”);
  • a queue that stops receiving traffic disappears from the exposition about five minutes later, rather than reporting zero.

queen_parked_long_polls is the exception: it is the live, instantaneous per-queue count of parked long-poll pops on this instance.

Per-queue series are not tenant-scoped

No series on this endpoint carries a tenant label. When QUEEN_TENANCY_HEADER is on and two tenants hold the same queue name, their series collide:

  • queen_dlq_depth_by_queue groups dead-letter rows by queue name, so the count is the sum across tenants;
  • queen_parked_long_polls explicitly sums the per-tenant gauges into one line per queue name;
  • the per-queue minute rates are stored per tenant but the aggregator selects one row per queue name, so one tenant’s bucket is reported and the others are hidden.

Two families you cannot alert on

queen_queue_depth_total and queen_queue_depth_pending appear in the exposition code, gated on a queue_depth key in the aggregator’s JSON. No stored procedure in this tree produces that key, so neither family is present on a real scrape. Do not build a backlog alert on them; use queen_queue_pop_lag_milliseconds and the consumer-group lag endpoints instead.

queen_dlq_depth and queen_dlq_depth_by_queue do work: they count queen.log_dlq rows, cluster total and per queue name, with an index-only scan per scrape. (Until 2026-07-31 they read the retired rows engine’s dead-letter table and were pinned at 0; if a dashboard predates that, its DLQ panel was lying.) queen_cluster_dlq_total{scope="cluster"} remains the lifetime counter of dead-letter transitions; the depth gauges shrink when DLQ rows are deleted or replayed, the counter does not.

The families

GET /metrics/prometheus exposes 50 families. queen_process_* counts what this one broker instance did since it started; queen_cluster_* are lifetime totals read back out of PostgreSQL, so every instance reports the same value.

Process (this broker instance)

Family Type Help
queen_event_loop_lag_avg_milliseconds gauge Mean scheduler (event-loop) lag since start
queen_parked_long_polls gauge Currently parked long-poll pops on this process
queen_process_ack_messages_total counter Messages acked by this process
queen_process_ack_requests_total counter Ack API requests handled by this process
queen_process_pop_messages_total counter Messages popped by this process
queen_process_pop_requests_total counter Pop API requests handled by this process
queen_process_push_messages_total counter Messages pushed by this process
queen_process_push_requests_total counter Push API requests handled by this process
queen_process_resident_memory_bytes gauge Resident memory of this process
queen_uptime_seconds gauge Process uptime in seconds

Cluster lifetime totals (from PostgreSQL)

Family Type Help
queen_cluster_ack_messages_total counter DB-backed cluster lifetime total
queen_cluster_ack_requests_total counter DB-backed cluster lifetime total
queen_cluster_ack_total counter Acks by outcome (DB-backed)
queen_cluster_db_errors_total counter DB-backed cluster lifetime total
queen_cluster_dlq_total counter DB-backed cluster lifetime total
queen_cluster_pop_messages_total counter DB-backed cluster lifetime total
queen_cluster_pop_requests_total counter DB-backed cluster lifetime total
queen_cluster_push_messages_total counter DB-backed cluster lifetime total
queen_cluster_push_requests_total counter DB-backed cluster lifetime total
queen_cluster_transactions_total counter DB-backed cluster lifetime total

Per-queue rates and depth

Family Type Help
queen_dlq_depth gauge Dead-letter queue depth
queen_dlq_depth_by_queue gauge Dead-letter depth per queue
queen_queue_ack_per_minute gauge Per-queue acks by result
queen_queue_depth_pending gauge Pending (unconsumed) messages per queue
queen_queue_depth_total gauge Total messages stored per queue
queen_queue_metrics_age_seconds gauge Per-queue minute-rate
queen_queue_parked_consumers gauge Per-queue minute-rate
queen_queue_pop_empty_per_minute gauge Per-queue minute-rate
queen_queue_pop_lag_milliseconds gauge Per-queue pop lag
queen_queue_pop_messages_per_minute gauge Per-queue minute-rate
queen_queue_push_messages_per_minute gauge Per-queue minute-rate
queen_queue_push_requests_per_minute gauge Per-queue minute-rate
queen_queue_transactions_per_minute gauge Per-queue minute-rate

Engine internals

Family Type Help
queen_batch_items_fired_total counter Items flushed across fusion batches
queen_batch_rtt_milliseconds gauge Fusion batch round-trip latency
queen_batches_fired_total counter Fusion batches flushed
queen_fusion_items_per_batch gauge Mean items per fusion batch
queen_pop_fill_wait_microseconds_total counter Total time pops spent fattening an under-full batch
queen_pop_fill_wait_total counter Pops that held an under-full batch back (minPopWaitTime)
queen_pop_targeted_total counter Hinted targeted single-partition pops issued
queen_pop_wildcard_total counter Wildcard candidate-scan pops issued
queen_seg_pop_vegas_limit gauge Adaptive pop concurrency limit
queen_seg_push_vegas_limit gauge Adaptive push concurrency limit

Other

Family Type Help
queen_db_pool_active gauge Active pooled connections
queen_db_pool_idle gauge Idle pooled connections
queen_db_pool_size gauge Configured DB pool size
queen_file_buffer_db_healthy gauge File-buffer DB-reachability hint
queen_file_buffer_failed gauge Spool write failures
queen_file_buffer_pending gauge Spooled push events awaiting drain
queen_maintenance_mode_enabled gauge Push maintenance mode flag
Navigation

Type to search…

↑↓ navigate↵ selectEsc close