Fan out N requests with controlled concurrency. Never overwhelm a server.
asyncio.gather() to run them all concurrently.
# All 100 requests at once
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
results = await asyncio.gather(*tasks) # โ BOOM, server hammered
semaphore = asyncio.Semaphore(5) # Counter = 5
# First 5 to call acquire() succeed immediately
async with semaphore:
# Counter: 5 โ 4
await fetch(url)
# Counter: 4 โ 5 (on exit)
# The 6th waits here
async with semaphore:
# Wait until counter > 0
# Counter: 5 โ 4
await fetch(url)
# Counter: 4 โ 5 (on exit)
async with semaphore.
import asyncio
import httpx
async def fetch_one(client, semaphore, url, i):
async with semaphore: # โ waits here if 5 already running
response = await client.get(url, timeout=10)
response.raise_for_status()
return f"[{i}] {response.json()['fact']}"
async def main():
semaphore = asyncio.Semaphore(5) # max 5 concurrent
async with httpx.AsyncClient() as client:
urls = ["https://catfact.ninja/fact"] * 100
tasks = [fetch_one(client, semaphore, url, i) for i, url in enumerate(urls)]
results = await asyncio.gather(*tasks)
for result in results:
print(result)
asyncio.run(main())
A counter that controls access to a resource. Max N things can run; the (N+1)th waits.
Fan-out N requests but cap concurrency (avoid overwhelming servers, DBs, or I/O).
Create Semaphore(max), wrap async work with async with semaphore.
Same overlap as gather() without all N at once. Respectful, sustainable concurrency.