Fifty PHP-FPM workers all waiting on one slow upstream is a site that is down because of a service most of its pages do not use.
// workers ÷ upstream latency = the maximum request rate.
// 50 workers, a 20s upstream: 2.5 req/s before exhaustion.
// a semaphore in Redis, capping concurrent calls to ONE dependency
if (! $this->semaphore->acquire('payments', $limit = 10, $ttl = 5)) {
throw new UpstreamBusy('payments');
}
try {
return $this->gateway->authorise($card);
} finally {
$this->semaphore->release('payments');
}
Rejecting immediately when the cap is reached is the point — a queue behind the bulkhead is the worker pool again with extra steps. The TTL on the semaphore entry is what recovers from a worker that died holding one, and getting it wrong in either direction is either a leak or a cap that does not cap. A timeout bounds one request and only a concurrency cap bounds the pool, which is the arithmetic people miss.