Gopher Systems Journal
Independent Go engineering notes · Daily edition
Resilience · Capacity

Concurrency Limits Belong at Every Scarce Resource

Why one global semaphore is not enough, and how Go services can isolate pressure by dependency.

October 22, 202610 min readIssue 0747

Find the resource behind the goroutine

Goroutines are cheap; the database connections, file descriptors, remote quotas, and memory they consume are not. A service-wide limit can prevent collapse, but it lets one slow dependency starve unrelated traffic. Place limits near each scarce resource and add a broader admission limit only when the process needs an overall safety envelope.

select {
case limiter <- struct{}{}:
    defer func() { <-limiter }()
case <-ctx.Done():
    return context.Cause(ctx)
}
return callDependency(ctx)

Waiting consumes the caller's budget

Semaphore acquisition must honor cancellation. Record queue duration separately from execution time so a saturated limiter is visible. Fairness matters when requests have different costs: a simple token channel may be sufficient for uniform calls, while weighted work needs a policy that prevents large requests from waiting forever.

Align limits with downstream pools. Allowing 500 handlers to compete for 20 database connections creates an invisible second queue inside the driver. A small application-side limit can reject or shed earlier, where the service still knows request priority. Avoid independent retry storms on top of these queues.

Capacity is a feedback loop

Track in-flight work, wait time, rejections, dependency latency, and success rate. Increase a limit only when the resource has headroom and the workload benefits. The goal is not maximum concurrency; it is stable useful throughput with bounded latency.

Today's implementation checklist

Before changing production code, write down the resource being protected, the failure signal a caller will see, and the metric that will prove improvement. Prefer a small reversible change. Exercise cancellation and overload paths, then review the resulting profile or trace beside the baseline. The discipline is intentionally plain: make ownership visible, put a bound on waiting, and preserve enough evidence to explain the outcome tomorrow.