Creating thousands of coroutines does not mean an API, database, or pool can handle thousands of simultaneous operations. asyncio.Semaphore tracks permits and makes new tasks wait when the bound is reached.
import asyncio
async def processar(item: int, limite: asyncio.Semaphore) -> int:
async with limite:
await asyncio.sleep(0.1)
return item * 2
async def main() -> None:
limite = asyncio.Semaphore(5)
resultados = await asyncio.gather(
*(processar(item, limite) for item in range(20))
)
print(resultados)
asyncio.run(main())
The async with block acquires and releases the permit safely. Choose the value from the real constraint, such as connection-pool size or a documented service limit, then measure latency, errors, and backlog before increasing it.
Practical guidance
A semaphore controls instantaneous concurrency, not requests per second. Use a proper rate-limiting policy for time-based quotas. Keep only the constrained operation inside the block so local work does not hold permits. Consider BoundedSemaphore when accidental over-release should raise an error.
Use asyncio.TaskGroup for related task lifecycles. For HTTP calls, align limits with the client described in Python HTTPX.
The official asyncio synchronization primitives documentation, accessed July 22, 2026, documents the API and its constraints.