An asyncio semaphore limits simultaneous entries into a critical section; it does not bound the number of waiting tasks.
Python asyncio Semaphore: bound active work, not queued tasks
Operation contract
A delivery-status client permits only two active requests to a downstream service. The semaphore holds that limit, but all four scheduled tasks still exist and can retain their inputs while waiting. For an unbounded feed, admit jobs through a bounded queue before creating tasks. Task groups own the child lifetimes; they do not impose admission limits by themselves.
Failure and ownership boundary
Cancellation inside async with releases a permit as the context exits. A coroutine that catches CancelledError for cleanup should normally propagate it after cleanup; suppressing it can confuse structured concurrency. A semaphore does not turn a blocking library call into cooperative I/O. The program deliberately uses sleep(0) to yield while the limit is held, making the observed peak repeatable.
Working program
import asyncio
async def main():
gate = asyncio.Semaphore(2)
active = 0
peak = 0
async def fetch_status(parcel_id):
nonlocal active, peak
async with gate:
active += 1
peak = max(peak, active)
await asyncio.sleep(0)
active -= 1
return parcel_id
async with asyncio.TaskGroup() as group:
tasks = [group.create_task(fetch_status(parcel_id))
for parcel_id in ("P-47", "P-48", "P-49", "P-50")]
print("scheduled:", len(tasks))
print("peak:", peak)
print("results:", [task.result() for task in tasks])
asyncio.run(main())Output
scheduled: 4
peak: 2
results: ['P-47', 'P-48', 'P-49', 'P-50']Costs and limits
The permit count is bounded, but task memory grows with admitted inputs. A fixed-capacity queue is required to bound waiting payloads.
Common Mistakes
- A semaphore does not cap the number of created tasks.
- Do not swallow cancellation and claim the request stopped.
- Async syntax does not make synchronous blocking I/O cooperative.
Connected lessons
Python asyncio.Queue: backpressure and completion accounting, Python asyncio TaskGroup: cancel sibling work and retain failure evidence, Python asyncio timeout: cancellation and cleanup ownership.
Test this contract in the Python concurrency and storage boundary quiz.
