Gatekeeping a Limited Resource Pool with a Counting Semaphore
Use a counting semaphore to cap concurrent access to a finite resource like an API or connection pool, preventing exhaustion while managing blocking, leaks, and ordering risks.
12 Oct 2026, 18:02 UTC

Unbounded concurrency exhausts a finite resource
Spawning a task for every incoming request is simple until those tasks depend on a finite resource. A database connection pool, a hardware license count, or a third‑party API with a concurrency cap will saturate. Beyond the cap you see connection refusals, timeouts, and cascading back‑pressure rather than more throughput.
A counting semaphore provides a deliberate cap. It maintains an internal permit counter and lets only N operations proceed concurrently. Additional callers wait without consuming CPU until a permit is released.
How acquire and release regulate flow
A counting semaphore is defined by an initial permit count. acquire decrements the counter if it is greater than zero and allows the caller to proceed. If the counter is zero, the caller blocks until a release occurs.
release increments the counter and may wake a blocked caller. The wake order is implementation defined; some implementations offer a fairness option to avoid starvation.
Binary semaphore versus mutex ownership
A binary semaphore with one permit behaves similarly to a mutex for exclusive access, but ownership differs. A mutex must be released by the same thread that acquired it. A binary semaphore can be released by a different thread, which makes it suitable for signaling, for example one thread waiting for another to finish initialization.
Worked example: capping concurrent external calls
Assume an external service tolerates at most three concurrent requests. A counting semaphore with three permits can enforce that limit at the client.
// Pseudocode, language‑agnostic. Replace with the semaphore API available in your runtime.
// Initialization, run once at process start with permission to create synchronization primitives
semaphore = CountingSemaphore(initial_permits = 3)
function handleRequest(requestId):
// Acquire blocks if three callers already hold permits
acquire(semaphore)
try:
// Critical section: at most three executions overlap
result = callExternalService(requestId)
return result
finally:
// Release must be in a finally/defer path to avoid permit leak
release(semaphore)
Where to run: in the request handling path of the service that owns the external call. Required permissions: ability to create and manipulate in‑process synchronization primitives. Meaningful placeholders: initial_permits reflects the provider limit, requestId is for logging.
Expected checks: the permit counter never drops below zero, and the number of concurrent callExternalService executions never exceeds initial_permits. Risk: if release is skipped on error, the counter drifts down and the system can deadlock.
Trade‑offs and limitations
Semaphores prevent exhaustion but introduce new failure modes.
- Permit leaks. Missing a
releaseon an exceptional path permanently reduces available permits. Always pairacquirewith a guaranteedrelease. - Deadlock from ordering. Acquiring multiple semaphores in different orders across threads can deadlock. Choose a global acquisition order and document it.
- Priority inversion. A low‑priority holder can block a high‑priority waiter. This is a property of the scheduler and semaphore implementation, not the algorithm itself.
- Counter overflow. Releasing more times than acquiring can raise the counter above the intended maximum, defeating the cap. Validate that
releasecalls are balanced withacquirecalls.
Verification without assuming results
Check behavior with observable signals rather than assumed outcomes.
- Saturation check. Submit more concurrent requests than permits and observe blocking. Use a debugger or logs to confirm that
acquireblocks when the counter is zero and unblocks after arelease. - Non‑blocking attempt. Use
tryAcquireif available. Verify that it returns a boolean indicating success or failure without blocking, and that the caller handles the failure case explicitly. - Leak audit. Monitor the current permit count over a long run under steady load. A monotonic decline suggests a leak in error paths.
These checks confirm the gatekeeping property and highlight where release guarantees are missing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.