Scaling workers on time-to-start rather than depth

Queue depth without throughput says nothing — a hundred jobs is fine at fifty a second and an incident at one a minute.

// the metric: the age of the oldest waiting job
foreach (['interactive', 'bulk'] as $queue) {
    $gauge->set($this->oldestPendingAgeSeconds($queue), ['queue' => $queue]);
}

// and the scaling rule, per queue
//   interactive: scale up above 30s, down below 5s
//   bulk:        scale up above 30m, down below 5m

Oldest-waiting-age measures the promise directly and needs no interpretation, where depth needs dividing by a throughput nobody has to hand. Different thresholds per queue is the reason for having split them, and a heartbeat job enqueued on a timer gives the same signal without instrumenting broker internals.