Python aiohttp: Limit the number of concurrent requests

Updated: January 2, 2024 By: Guest Contributor Post a comment

Introduction

When working with asynchronous HTTP requests in Python, managing the number of concurrent requests is essential to maintain system stability and adhere to server rate limits. The Python aiohttp library allows for asynchronous HTTP requests and can be customized to limit concurrency.

Understanding aiohttp

aiohttp is an asynchronous HTTP client/server framework that uses asyncio at its core. It enables non-blocking network communication, allowing I/O-bound tasks to be handled efficiently. Before we discuss limiting requests, it’s important to understand the basic creation and execution of an aiohttp session and the sending of HTTP requests.

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'http://python.org')
        print(html)

if __name__ == '__main__':
    asyncio.run(main())

Creating a Basic Semaphore

A semaphore is a synchronization mechanism that can be used to constrain concurrent access to a resource. In our case, it will be used to limit the number of concurrent requests. Utilizing asyncio.Semaphore, we create a semaphore limited to a specific number of permits.

import aiohttp
import asyncio

# Global semaphore
SEMAPHORE = asyncio.Semaphore(5)

async def fetch_with_limit(semaphore, session, url):
    async with semaphore:
        async with session.get(url) as response:
            return await response.read()

Here, the semaphore parameter is passed to the fetch_with_limit function. The permits availability manages the number of concurrent fetch_with_limit coroutines. Given that our semaphore has a value of 5, no more than 5 instances of fetch_with_limit can execute concurrently.

See also: Python asyncio.Semaphore class (with examples).

Implementing Semaphore-Controlled Requests

With the semaphore defined, let’s integrate it into an aiohttp session. We’ll be using a list of URLs for this example. The semaphore is used within the context manager to restrict concurrent fetch calls.

import aiohttp
import asyncio

SEMAPHORE = asyncio.Semaphore(5)

async def fetch_with_limit(semaphore, session, url):
    async with semaphore:
        async with session.get(url) as response:
            return await response.read()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls:
            task = fetch_with_limit(SEMAPHORE, session, url)
            tasks.append(task)
        results = await asyncio.gather(*tasks)
        return results

if __name__ == '__main__':
    urls = ['http://python.org', 'http://asyncio.org'] * 10
    result = asyncio.run(main(urls))
    print(result)

Error Handling and Timeout Management

When limiting requests, implementing robust error handling and managing timeouts is vital. By doing so, we prevent individual request failures from blocking our entire queue of requests.

import aiohttp
import asyncio
from aiohttp import ClientError, ClientTimeout

SEMAPHORE = asyncio.Semaphore(5)

async def fetch_with_limit(semaphore, session, url):
    try:
        async with semaphore:
            async with session.get(url, timeout=ClientTimeout(total=30)) as response:
                return await response.read()
    except ClientError as e:
        # Handle specific HTTP errors and timeouts here
        print(f'Error fetching {url}: {e}')
        return None

Conclusion

In summary, limiting the number of concurrent requests in aiohttp is a vital technique for both respecting server-side rate limits and keeping client-side resources under control. Through the use of async functions, the ClientSession, and Semaphores, one can efficiently manage concurrent requests, ensuring the application remains stable and performant. Remember to always handle errors gracefully and adjust the concurrent limit appropriately to match the nature of your I/O-bound tasks and the requirements of the servers you are communicating with.