RabbitMQ head message age: the queue latency that depth alone cannot show
A queue with 10 messages whose oldest is 3 hours old is in worse shape than a queue with 10,000 messages whose oldest is 5 seconds old. Queue depth tells you volume. It says nothing about how long messages are actually waiting. Operators who alert on depth alone end up paged for healthy batch queues and blind to a real-time queue that has been stalled for an hour.
head_message_timestamp closes that gap. It is the timestamp of the message currently at the head of the queue, and the difference between now and that timestamp is your queue’s latency floor: the minimum time any message in that queue has been waiting. It is also the signal that catches the two failure modes depth structurally misses: poison-message stalls and slow-consumer drift on low-volume queues.
It has real limitations, and RabbitMQ maintainers have said so publicly. The sections below cover what it measures and the cases where it misleads you.
What it is and why it matters
head_message_timestamp is a per-queue info item exposed by the broker:
- HTTP Management API:
GET /api/queues, fieldhead_message_timestamp(Unix epoch seconds) - CLI:
rabbitmqctl list_queues name head_message_timestamp - Prometheus plugin:
rabbitmq_queue_head_message_timestamp
The value is not computed by the broker. It is copied from the timestamp property of whichever message happens to be at the head of the queue right now. That distinction matters for everything that follows.
Head message age is computed by your monitoring, not by RabbitMQ:
# Head message age for every queue, in seconds
curl -s -u guest:guest http://localhost:15672/api/queues | \
jq -r --argjson now "$(date +%s)" \
'.[] | select(.head_message_timestamp != null) |
"\(.name) age=\($now - .head_message_timestamp)s ready=\(.messages_ready)"'
Note that the default guest user can only authenticate from localhost; against a remote broker use a monitoring user with the monitoring tag.
The reason this signal earns a place next to depth is that it inverts cleanly per workload. A batch queue draining 10,000 messages with a 5-second-old head is healthy. A real-time queue holding 10 messages with a 3-hour-old head is an incident. Depth-based alerts cannot express that difference. Age-based alerts express it directly: “messages in queue X should never be older than Y seconds.”
A reasonable starting heuristic: age over 10 minutes for real-time queues, age over 1 hour for batch queues. Beyond that, set the threshold from the per-queue SLA.
How it works
The data path has three hops, and the signal can break at each one:
flowchart LR P[Publisher sets timestamp property] --> B[Broker stores message] B --> H[Message reaches queue head] H --> A[API exposes head_message_timestamp] A --> M[Monitoring computes now minus timestamp] T[TTL expiry or dead-lettering] -. resets head .-> H R[Consumer nack with requeue] -. original timestamp returns to head .-> H
- The publisher sets the timestamp. The value comes from the AMQP
timestampmessage property, set by the publishing client. If your publishers do not set it, the field is absent or null and the signal does not exist. Many client libraries and applications never set it. - The message reaches the head. Only the head message’s timestamp is reported. You learn nothing about the second message until the first one leaves.
- The message must be paged in. Per the rabbitmqctl man page, timestamps only appear when the head message is in the paged-in state, meaning resident in RAM. Under memory pressure, when the broker is paging aggressively, the field can go absent exactly when things are getting bad. See paging messages to disk for that mechanism.
Because the value is publisher-supplied, you can fix an absent signal two ways. The right fix is publishers setting the timestamp property. The broker-side fix, for 3.12 and later, is the built-in incoming message interceptor in rabbitmq.conf:
# Stamp the arrival time on messages that lack a timestamp property
message_interceptors.incoming.set_header_timestamp.overwrite = false
With overwrite = false, publisher-supplied timestamps are preserved and only missing ones are filled in. This replaces the rabbitmq-message-timestamp plugin, which is deprecated as of 3.12.
Where it shows up in production
Three patterns are where head message age pays for itself.
Poison-message stalls. A message the consumer cannot process gets delivered, nacked, requeued, delivered again. Queue depth stays flat because nothing is completing and nothing new is stuck. The only signals that move are the redeliver rate and the head message age climbing minute after minute. The signature: stable depth, rising head age, elevated redeliver, unacked count pinned at a small number. Depth alerting sees nothing. Age alerting pages you.
Slow-consumer drift on low-volume queues. A queue that gets one message a minute will never trip a depth threshold, but if consumers degrade, the head age trends upward for hours before depth becomes interesting. Age is the leading indicator; depth is the lagging one.
Distinguishing backlog from latency during incidents. During a traffic spike, depth and age together tell you whether you have a capacity problem (deep queue, young head: consumers are keeping up with latency, just behind on volume) or a stall (any depth, old head: something is stuck). These have different fixes.
When the signal lies to you
This metric has documented reliability problems. RabbitMQ maintainers have described it as not working “all that well in practice.” Treat it as a strong signal with known blind spots, not ground truth.
Quorum queues. This is the biggest operational gotcha. Historically head_message_timestamp was not populated for quorum queues, and maintainers have cited the resource cost of tracking it in the Raft log path. Behaviour here is version-dependent, and since quorum queues are the recommended queue type on 3.13+, verify it on your version before building alerts on it. For quorum queues, the fallback is non-destructive inspection of the head message with rabbitmqadmin get queue=<queue-name> count=1, which shows message headers including any timestamp. That is a manual check, not a metric.
Absent publisher timestamps. Covered above: no timestamp property, no signal. Verify before you build alerts on it.
TTL and dead-lettering reset the head. When the head message expires via TTL and is dead-lettered or dropped, the next message becomes head and the age drops. A queue with a persistent consumer stall and a short TTL can show a young head forever while messages silently expire. Age going down is not always recovery; check whether messages are being consumed or being discarded.
Requeue preserves the original timestamp. A message nacked with requeue=true returns to the head carrying its original publish time. That is usually what you want, since it reflects true time-in-system, but it means head age measures “age of the message at the head” and not “time since the queue last made progress.” In a redelivery loop, the age keeps climbing, which is precisely the poison-message signature.
Paging can blank it out. When memory pressure pages the head message to disk, the timestamp can go absent during exactly the incidents where you want it. If your age series has gaps, correlate with messages_paged_out and the memory ratio before concluding the queue drained.
Empty queues have no head. No messages, no timestamp. Your alerting must treat “field absent” as its own state, distinct from “age is zero,” or you will either spam false alerts or silence real ones.
Signals to read alongside it
Head message age is only interpretable in combination:
| Signal | Why it matters | Warning sign |
|---|---|---|
| Queue depth (ready + unacked) | Depth + age together give the complete picture: volume and latency | Depth flat or rising while age climbs steadily |
| Redeliver rate per queue | Distinguishes poison-message loop from slow consumer | Elevated redeliver + rising age + flat depth = poison message |
| Ack rate | Confirms whether consumers are completing work | Deliver rate positive, ack rate zero, age rising = stuck consumer |
| Consumer count and consumer_utilisation | Tells you whether consumers exist and are effective | Utilisation under 0.5 with backlog and rising age |
| messages_paged_out | Explains gaps or nulls in the age series | Paged-out count rising while age field goes absent |
The two shapes worth memorizing: rising age with rising depth is a capacity problem (add consumers or speed them up). Rising age with flat depth and elevated redeliver is a poison message (inspect the head message, dead-letter it, fix the consumer).
How Netdata helps
Netdata’s RabbitMQ collector pulls per-queue state from the Management API, so the correlation work above happens on one dashboard instead of three terminals:
- Per-queue
messages_readyandmessages_unacknowledgedcharted over time, so you can see the flat-depth or rising-depth shape next to any age signal you compute. - Per-queue message rates including redeliver, which is the companion signal that separates a poison-message loop from a slow consumer.
- Ack versus deliver rates per queue, exposing consumers that receive but never complete.
- Consumer counts per queue, so “age rising” is immediately checkable against “did anyone’s consumers just disappear.”
messages_paged_outper queue, which explains why a head-age series went null during memory pressure.
Per-queue collection is optional in the Netdata collector (collect_queues_metrics); make sure it is enabled for the vhosts you care about, and be deliberate about it on deployments with thousands of queues because of cardinality. Whether a given collector version surfaces the head timestamp itself varies, so the practical pattern is: let Netdata chart depth, rates, and paging, compute age from the API or Prometheus endpoint, and alert on the combination. Depth plus age is the complete picture; either one alone is half of it.
Related guides
- RabbitMQ monitoring checklist: the signals every production broker needs
- RabbitMQ monitoring maturity model: from survival to expert
- RabbitMQ paging messages to disk: the paging ratio as an early memory warning
- How RabbitMQ actually works in production: a mental model for operators
- RabbitMQ flow control vs resource alarms: the two throttling mechanisms operators confuse






