Semaphores and rate limits
gather starts everything at once. Against a real server, a thousand simultaneous requests get you throttled or blocked. An asyncio.Semaphore(n) holds n permits: async with sem: takes one, waiting if none are left, and gives it back when the block ends.
import asyncio
sem = asyncio.Semaphore(2)
async def fetch(n):
async with sem:
await asyncio.sleep(0.01)
return n * n
async def main():
jobs = [fetch(n) for n in range(5)]
print(await asyncio.gather(*jobs))
asyncio.run(main())All five tasks start, but only two at a time get past async with. The rest wait their turn.
One limit per server
A limit usually belongs to a resource, such as one host, not to the whole program. Keep one semaphore per resource in a dict, made the first time it is needed. Share it: a semaphore created inside each task limits nothing.
Your task
Write async def crawl(urls, fetch, per_host) that calls fetch(url) for every URL at once, but never with more than per_host calls running for the same host, urllib.parse.urlsplit(url).hostname. Return the results as a list in the order of urls.
The explanation opens here once every check passes.