Async HTTP with aiohttp
HTTP is the canonical I/O-bound workload, and aiohttp is the de-facto async HTTP client for Python. A single ClientSession manages the connection pool and TLS state; requests are coroutines, so a hundred fetches overlap on one thread. This chapter covers the client; the same session model underlies the aiohttp server you meet in web frameworks.
Single Request
import asyncio
import aiohttp
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.github.com") as resp:
print(resp.status)
data = await resp.json()
print(data)
asyncio.run(main())
The session is a context manager: entering it opens the connector, exiting it closes all pooled connections. The response is also async - await resp.json(), await resp.text(), or await resp.read() consume the body.
Fan-Out with gather
import asyncio
import aiohttp
URLS = [f"https://api.github.com/repos/python/cpython/tags?page={i}"
for i in range(1, 6)]
async def fetch(session, url):
async with session.get(url) as resp:
return url, resp.status, len(await resp.read())
async def main():
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*(fetch(session, u) for u in URLS))
for url, status, size in results:
print(url[-12:], status, size)
asyncio.run(main())
All five requests are in flight simultaneously; total latency is roughly the slowest one, not the sum. For rate-limited APIs, wrap each call in a semaphore (as in the previous chapter) and set connector=aiohttp.TCPConnector(limit=10) to bound open connections.
Errors and Timeouts
import asyncio
import aiohttp
TIMEOUT = aiohttp.ClientTimeout(total=10)
async def safe_get(session, url):
try:
async with session.get(url, timeout=TIMEOUT) as resp:
resp.raise_for_status()
return await resp.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
print("failed", url, exc)
return None
Always set a total timeout - an async request without one can suspend a task forever on a dead connection. Catch ClientError (connection failures, DNS) and TimeoutError separately from HTTPError, which raise_for_status() raises for 4xx/5xx responses you may want to retry.
Servers
The same library provides an async server: define a handler coroutine per route and run it with web.run_app. In practice most applications use a framework (FastAPI, Starlette) on top of the same async model, so the client skills here transfer directly to reading and writing async web APIs.
Summary
Use one ClientSession per program (or per worker), fan out requests with gather, cap concurrency with a connector limit and a semaphore, and give every request a total timeout. That combination is the standard recipe for fast, polite async scraping and API aggregation.