When using the requests library to fetch 100 URLs, your script waits for each round-trip to complete before starting the next. With aiohttp, you can fire all 100 requests simultaneously and handle them as they return. This approach is often 10x-50x faster for I/O-bound operations.
This guide covers the essential patterns for building efficient async HTTP clients in Python.
Basic Usage: Creating a Session
In aiohttp, creating a ClientSession is relatively expensive. You should create one session and reuse it for all requests within a context.
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("https://httpbin.org/get") as response:
print(f"Status: {response.status}")
data = await response.json()
print(data)
if __name__ == "__main__":
asyncio.run(main())Output:
Status: 200
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.9 aiohttp/3.13.3', 'X-Amzn-Trace-Id': 'Root=1-6991d2c1-65e778a610980e5729abfa98'}, 'origin': '111.95.13.231', 'url': 'https://httpbin.org/get'}Making Different Types of Requests
Aiohttp supports all standard HTTP methods:
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("https://httpbin.org/get") as resp:
get_data = await resp.json()
payload = {"username": "alice", "email": "alice@example.com"}
async with session.post("https://httpbin.org/post", json=payload) as resp:
post_data = await resp.json()
async with session.put("https://httpbin.org/put", json=payload) as resp:
put_data = await resp.json()
async with session.delete("https://httpbin.org/delete") as resp:
delete_data = await resp.json()
print("All requests completed successfully")
asyncio.run(main())Fetching Multiple URLs Concurrently
Use asyncio.gather to run multiple requests in parallel:
import aiohttp
import asyncio
import time
async def fetch(session: aiohttp.ClientSession, url: str) -> dict:
async with session.get(url) as response:
return {
"url": url,
"status": response.status,
"length": len(await response.text())
}
async def main():
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
start = time.perf_counter()
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
elapsed = time.perf_counter() - start
for result in results:
print(result)
print(f"\nFetched {len(urls)} URLs in {elapsed:.2f} seconds")
asyncio.run(main())Output:
{'url': 'https://httpbin.org/delay/1', 'status': 200, 'length': 361}
{'url': 'https://httpbin.org/delay/1', 'status': 200, 'length': 361}
{'url': 'https://httpbin.org/delay/1', 'status': 200, 'length': 361}
{'url': 'https://httpbin.org/delay/1', 'status': 200, 'length': 361}
Fetched 4 URLs in 1.15 secondsRate Limiting with Semaphores
Firing thousands of requests simultaneously can overwhelm servers or trigger rate limits. Use asyncio.Semaphore to control concurrency:
import aiohttp
import asyncio
async def fetch(
session: aiohttp.ClientSession,
url: str,
semaphore: asyncio.Semaphore
) -> dict:
async with semaphore:
try:
async with session.get(url) as response:
content = await response.text()
return {"url": url, "status": response.status, "length": len(content)}
except Exception as e:
return {"url": url, "error": str(e)}
async def main():
urls = [f"https://httpbin.org/get?page={i}" for i in range(50)]
semaphore = asyncio.Semaphore(10)
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url, semaphore) for url in urls]
print(f"Starting {len(urls)} requests (max 10 concurrent)...")
results = await asyncio.gather(*tasks)
successful = sum(1 for r in results if "error" not in r)
print(f"Completed: {successful}/{len(results)} successful")
asyncio.run(main())Output:
Starting 50 requests (max 10 concurrent)...
Completed: 50/50 successfulConfiguring Timeouts
Async requests can hang indefinitely without proper timeout configuration:
import aiohttp
import asyncio
async def main():
timeout = aiohttp.ClientTimeout(
total=30, # Total timeout for the entire operation
connect=10, # Timeout for establishing connection
sock_read=10 # Timeout for reading a chunk of data
)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.get("https://httpbin.org/delay/5") as resp:
data = await resp.json()
print(data)
except asyncio.TimeoutError:
print("Request timed out!")
asyncio.run(main())Output:
{'args': {}, 'data': '', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.9 aiohttp/3.13.3', 'X-Amzn-Trace-Id': 'Root=1-6991d37f-504760915433820a3d4546fc'}, 'origin': '111.95.13.231', 'url': 'https://httpbin.org/delay/5'}Adding Headers and Authentication
import aiohttp
import asyncio
async def main():
headers = {
"User-Agent": "MyApp/1.0",
"Accept": "application/json",
"Authorization": "Bearer your-token-here"
}
async with aiohttp.ClientSession(headers=headers) as session:
async with session.get("https://httpbin.org/headers") as resp:
data = await resp.json()
print(data)
asyncio.run(main())Output:
{'headers': {'Accept': 'application/json', 'Accept-Encoding': 'gzip, deflate', 'Authorization': 'Bearer your-token-here', 'Host': 'httpbin.org', 'User-Agent': 'MyApp/1.0', 'X-Amzn-Trace-Id': 'Root=1-6991d3aa-5b78c1d619dabdaf0e52c078'}}Handling Errors Gracefully
import aiohttp
import asyncio
async def fetch_with_retry(
session: aiohttp.ClientSession,
url: str,
max_retries: int = 3
) -> dict:
for attempt in range(max_retries):
try:
async with session.get(url) as response:
if response.status == 200:
return {"url": url, "data": await response.json()}
elif response.status >= 500:
await asyncio.sleep(2 ** attempt)
continue
else:
return {"url": url, "error": f"HTTP {response.status}"}
except aiohttp.ClientError as e:
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
continue
return {"url": url, "error": str(e)}
return {"url": url, "error": "Max retries exceeded"}
async def main():
async with aiohttp.ClientSession() as session:
result = await fetch_with_retry(session, "https://httpbin.org/get")
print(result)
asyncio.run(main())Output:
{'url': 'https://httpbin.org/get', 'data': {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.9 aiohttp/3.13.3', 'X-Amzn-Trace-Id': 'Root=1-6991d3c1-470cdcd51842dd402b2198e4'}, 'origin': '111.95.13.231', 'url': 'https://httpbin.org/get'}}Comparison: requests vs aiohttp
| Feature | requests (Sync) |
aiohttp (Async) |
|---|---|---|
| Speed | Sequential, one at a time | Concurrent, many at once |
| Complexity | Simple, one-liners | Requires async/await |
| Session | Optional | Essential for performance |
| Memory | Higher for many requests | More efficient |
| Best For | Simple scripts, few requests | Scrapers, APIs, microservices |
Summary
- Reuse sessions: Create one
ClientSessionand use it for all requests. - Use
asyncio.gather: Run multiple requests concurrently for massive speed gains. - Limit concurrency: Use
asyncio.Semaphoreto avoid overwhelming servers. - Set timeouts: Always configure timeouts to prevent hanging requests.
- Handle errors: Implement retry logic for resilient applications.