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.

Leave a Reply

Your email address will not be published. Required fields are marked *