Queue depth without throughput says nothing: a hundred jobs is fine at fifty a second and an incident at one a minute.
// the age of the oldest waiting job, per queue
foreach (['interactive', 'bulk'] as $queue) {
$gauge->set(
$this->redis->oldestPendingAge($queue),
['queue' => $queue]
);
}
// alert on THIS, not on depth:
// interactive > 30s → page
// bulk > 30m → ticket
Oldest-waiting-age is a direct measure of the promise being broken and needs no interpretation, where depth needs dividing by a throughput nobody has to hand. Different thresholds per queue is the point of having split them. A heartbeat job enqueued on a timer gives the same signal without instrumenting the queue internals, and works with any broker.