How to Optimize Load Performance in aiohttp
In this tutorial, you’ll learn various methods to optimize the performance of your aiohttp applications.
You’ll learn how to implement connection pooling, manage request concurrency, optimize database interactions, and fine-tune various aspects of the request-response cycle.
Reuse Connections for Multiple Requests
You can reuse the same session for multiple requests to improve performance:
import aiohttp import asyncio async def fetch_data(session, url): async with session.get(url) as response: return await response.json() async def main(): async with aiohttp.ClientSession() as session: urls = ['https://jsonplaceholder.typicode.com/posts/1', 'https://jsonplaceholder.typicode.com/users/1'] tasks = [fetch_data(session, url) for url in urls] results = await asyncio.gather(*tasks) print(results) asyncio.run(main())
Output:
[{'userId': 1, 'id': 1, 'title': 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', 'body': 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'}, {'id': 1, 'name': 'Leanne Graham', 'username': 'Bret', 'email': 'Sincere@april.biz', 'address': {'street': 'Kulas Light', 'suite': 'Apt. 556', 'city': 'Gwenborough', 'zipcode': '92998-3874', 'geo': {'lat': '-37.3159', 'lng': '81.1496'}}, 'phone': '1-770-736-8031 x56442', 'website': 'hildegard.org', 'company': {'name': 'Romaguera-Crona', 'catchPhrase': 'Multi-layered client-server neural-net', 'bs': 'harness real-time e-markets'}}]
Reusing the same session reduces the extra work needed to create new connections for each request.
Adjust Pool Size
You can adjust the connection pool size to optimize performance by setting the limit
parameter in the TCPConnector :
import aiohttp import asyncio async def main(): connector = aiohttp.TCPConnector(limit=20) async with aiohttp.ClientSession(connector=connector) as session: async with session.get('https://jsonplaceholder.typicode.com/users/2') as response: data = await response.json() print(data) asyncio.run(main())
Output:
{'id': 2, 'name': 'Ervin Howell', 'username': 'Antonette', 'email': 'Shanna@melissa.tv', 'address': {'street': 'Victor Plains', 'suite': 'Suite 879', 'city': 'Wisokyburgh', 'zipcode': '90566-7771', 'geo': {'lat': '-43.9509', 'lng': '-34.4618'}}, 'phone': '010-692-6593 x09125', 'website': 'anastasia.net', 'company': {'name': 'Deckow-Crist', 'catchPhrase': 'Proactive didactic contingency', 'bs': 'synergize scalable supply-chains'}}
This allows you to control the maximum number of concurrent connections.
Request Concurrency
Implement semaphores to limit concurrency
To limit the number of concurrent requests, you can use a semaphore:
import aiohttp import asyncio async def fetch_data(session, url, semaphore): async with semaphore: async with session.get(url) as response: return await response.json() async def main(): semaphore = asyncio.Semaphore(5) async with aiohttp.ClientSession() as session: urls = [f'https://jsonplaceholder.typicode.com/users/{i}' for i in range(10)] tasks = [fetch_data(session, url, semaphore) for url in urls] results = await asyncio.gather(*tasks) print(len(results)) asyncio.run(main())
Output:
10
The semaphore limits the number of concurrent requests to 5 while you still allow parallel execution.
Balance concurrency and server load
To balance concurrency and server load, you can adjust the semaphore value based on your server capacity:
import aiohttp import asyncio import psutil async def fetch_data(session, url, semaphore): async with semaphore: async with session.get(url) as response: return await response.json() async def main(): cpu_count = psutil.cpu_count() semaphore = asyncio.Semaphore(cpu_count * 2) async with aiohttp.ClientSession() as session: urls = [f'https://jsonplaceholder.typicode.com/posts/{i}' for i in range(50)] tasks = [fetch_data(session, url, semaphore) for url in urls] results = await asyncio.gather(*tasks) print(f"Processed {len(results)} requests with {cpu_count} CPUs") asyncio.run(main())
Output:
Processed 50 requests with 16 CPUs
This method sets the semaphore value to twice the number of CPU cores.
Database Optimization
Use asynchronous database drivers
To optimize database interactions, use asynchronous drivers like asyncpg:
import asyncio import asyncpg async def fetch_users(): conn = await asyncpg.connect('postgresql://user:password@localhost/database') try: users = await conn.fetch('SELECT * FROM users LIMIT 5') return [dict(user) for user in users] finally: await conn.close() async def main(): users = await fetch_users() print(users) asyncio.run(main())
Output:
[{'id': 1, 'name': 'Tom'}, {'id': 2, 'name': 'Adam'}, {'id': 3, 'name': 'Sam'}, {'id': 4, 'name': 'Alice'}, {'id': 5, 'name': 'Chris'}]
Asyncpg provides high-performance asynchronous operations for PostgreSQL databases.
Implement connection pooling for databases
To improve database performance, implement connection pooling:
import asyncio import asyncpg async def fetch_users(pool): async with pool.acquire() as conn: users = await conn.fetch('SELECT * FROM users LIMIT 5') return [dict(user) for user in users] async def main(): pool = await asyncpg.create_pool('postgresql://user:password@localhost/database', min_size=5, max_size=20) try: users = await fetch_users(pool) print(users) finally: await pool.close() asyncio.run(main())
Output:
[{'id': 1, 'name': 'Tom'}, {'id': 2, 'name': 'Adam'}, {'id': 3, 'name': 'Sam'}, {'id': 4, 'name': 'Alice'}, {'id': 5, 'name': 'Chris'}]
Connection pooling reduces the number of new database connections for each query.
Implement Response Compression
To reduce response size, implement compression:
from aiohttp import web import gzip async def handle(request): data = {'message': 'Hello, World!' * 1000} body = web.json_response(data).body return web.Response(body=gzip.compress(body), headers={'Content-Encoding': 'gzip', 'Content-Type': 'application/json'}) app = web.Application() app.router.add_get('/', handle) if __name__ == '__main__': web.run_app(app)
To test if compression reduces the amount of data transferred over the network, you can use the following client code:
import aiohttp import asyncio async def fetch_compressed_data(): async with aiohttp.ClientSession() as session: async with session.get('http://localhost:8080', headers={'Accept-Encoding': 'gzip'}) as response: print(f"Content-Encoding: {response.headers.get('Content-Encoding')}") print(f"Content-Length: {response.headers.get('Content-Length')}") return await response.json() async def main(): data = await fetch_compressed_data() print(f"Received {len(str(data))} characters of data") asyncio.run(main())
Output:
Content-Encoding: gzip Content-Length: 94 Received 13015 characters of data
Use Pipelining
To further optimize performance, you can implement HTTP pipelining:
import aiohttp import asyncio async def fetch_with_pipelining(): async with aiohttp.ClientSession() as session: urls = [f'https://jsonplaceholder.typicode.com/posts/{i}' for i in range(5)] async with session.get(urls[0]) as response: # Trigger pipelining by sending all requests before reading responses tasks = [session.get(url) for url in urls[1:]] responses = await asyncio.gather(response.json(), *tasks) for i, response in enumerate(responses): if isinstance(response, aiohttp.ClientResponse): data = await response.json() else: data = response print(f"Request {i + 1}: {data}") async def main(): await fetch_with_pipelining() asyncio.run(main())
Output:
Request 1: {} Request 2: {'userId': 1, 'id': 1, 'title': 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', 'body': 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'} Request 3: {'userId': 1, 'id': 2, 'title': 'qui est esse', 'body': 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla'} Request 4: {'userId': 1, 'id': 3, 'title': 'ea molestias quasi exercitationem repellat qui ipsa sit aut', 'body': 'et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut'} Request 5: {'userId': 1, 'id': 4, 'title': 'eum et est occaecati', 'body': 'ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit'}
HTTP pipelining sends multiple requests before waiting for responses to reduce latency.
Mokhtar is the founder of LikeGeeks.com. He is a seasoned technologist and accomplished author, with expertise in Linux system administration and Python development. Since 2010, Mokhtar has built an impressive career, transitioning from system administration to Python development in 2015. His work spans large corporations to freelance clients around the globe. Alongside his technical work, Mokhtar has authored some insightful books in his field. Known for his innovative solutions, meticulous attention to detail, and high-quality work, Mokhtar continually seeks new challenges within the dynamic field of technology.