python 36 lines · 8 steps

Bounded-concurrency HTTP fetching with asyncio

Fetch many URLs in parallel while capping in-flight requests with a semaphore, returning a structured result for each.

Explained by highlit
1import asyncio
2from dataclasses import dataclass
3 
4import aiohttp
5 
6 
7@dataclass
8class FetchResult:
9 url: str
10 status: int | None
11 body: str | None
12 error: str | None = None
13 
14 
15async def fetch_one(
16 session: aiohttp.ClientSession,
17 semaphore: asyncio.Semaphore,
18 url: str,
19 timeout: float = 10.0,
20) -> FetchResult:
21 async with semaphore:
22 try:
23 async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as resp:
24 return FetchResult(url=url, status=resp.status, body=await resp.text())
25 except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
26 return FetchResult(url=url, status=None, body=None, error=str(exc))
27 
28 
29async def fetch_all(urls: list[str], max_concurrency: int = 8) -> list[FetchResult]:
30 semaphore = asyncio.Semaphore(max_concurrency)
31 async with aiohttp.ClientSession() as session:
32 tasks = [
33 asyncio.create_task(fetch_one(session, semaphore, url))
34 for url in urls
35 ]
36 return await asyncio.gather(*tasks)
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A shared asyncio.Semaphore caps how many coroutines run their critical section at once, throttling concurrency without blocking the loop.
  2. 2Returning a result object per URL instead of raising lets partial failures coexist with successes in a single gathered list.
  3. 3Reusing one ClientSession across all requests amortizes connection pooling and TLS setup for every fetch.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Bounded-concurrency HTTP fetching with asyncio — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code