Sling Academy
Home/Python/Python aiohttp: Limit the number of requests per second

Python aiohttp: Limit the number of requests per second

Last updated: January 02, 2024

Overview

Managing the rate of requests is crucial when dealing with web services to avoid overwhelming servers and violating API rate limits. The aiohttp library in Python enables asynchronous requests, and this tutorial demonstrates how to rate-limit them.

Before diving into rate-limiting with aiohttp, ensure you have Python 3.7 or later installed with the aiohttp package. You can install aiohttp using pip install aiohttp.

Basic Rate-Limiting

Let’s begin with a basic example that sends HTTP requests and rate-limits them to a specified number per second.

import aiohttp
import asyncio
import time

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

async def rate_limited_requester(urls, rate):
    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls:
            task = asyncio.create_task(fetch(url, session))
            tasks.append(task)
            await asyncio.sleep(1 / rate)
        responses = await asyncio.gather(*tasks)
        return responses

urls = ['http://httpbin.org/get' for _ in range(10)]
rate = 2  # Two requests per second
loop = asyncio.get_event_loop()
loop.run_until_complete(rate_limited_requester(urls, rate))

Using a Semaphore

Another approach is to use a semaphore to limit the number of simultaneous requests. This can be combined with the rate limiting.

import aiohttp
import asyncio
import time

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

async def rate_limited_requester(urls, rate):
    async with aiohttp.ClientSession() as session:
        semaphore = asyncio.Semaphore(rate)
        tasks = []
        for url in urls:
            task = asyncio.create_task(fetch(url, session, semaphore))
            tasks.append(task)
        responses = await asyncio.gather(*tasks)
        return responses

urls = ['http://httpbin.org/get' for _ in range(10)]
rate = 2  # Two requests per second
loop = asyncio.get_event_loop()
loop.run_until_complete(rate_limited_requester(urls, rate))

You can read a dedicated guide to Semaphore here: Python asyncio.Semaphore class explained (with examples).

Advanced Techniques

For more precise rate-limiting, we can use third-party libraries such as aiolimiter, which provides an asynchronous leaky bucket algorithm implementation.

from aiolimiter import AsyncLimiter
import aiohttp
import asyncio

limiter = AsyncLimiter(max_rate=2, time_period=1)

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

async def main(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(url, session, limiter) for url in urls]
        return await asyncio.gather(*tasks)

urls = ['http://httpbin.org/get' for _ in range(20)]
loop = asyncio.get_event_loop()
loop.run_until_complete(main(urls))

Incorporating a retry mechanism or a backoff strategy when rate limits are reached can be an essential feature for a robust system. Here’s how to implement a simple exponential backoff in case of a 429 status (Too Many Requests) response.

import random

async def fetch_with_backoff(url, session, limiter, max_retries=5):
    for retry in range(max_retries):
        await limiter
        async with session.get(url) as response:
            if response.status == 429:
                backoff_time = 2 ** retry + random.uniform(0, 1)
                await asyncio.sleep(backoff_time)
            else:
                return await response.text()
    return 'Max retries reached'

# Rest of the code remains similar

Conclusion

This tutorial has walked you through several ways to rate-limit requests using the aiohttp library in Python. By applying these patterns, you can avoid API rate limits, make your application more robust, and protect the services you are interacting with from being overwhelmed.

Next Article: Python Requests Module: Exception Handling Best Practices

Previous Article: Python & aiohttp: How to create a simple web server

Series: Python: Network & JSON tutorials

Python

You May Also Like

  • Python Warning: Secure coding is not enabled for restorable state
  • Python TypeError: write() argument must be str, not bytes
  • 4 ways to install Python modules on Windows without admin rights
  • Python TypeError: object of type ‘NoneType’ has no len()
  • Python: How to access command-line arguments (3 approaches)
  • Understanding ‘Never’ type in Python 3.11+ (5 examples)
  • Python: 3 Ways to Retrieve City/Country from IP Address
  • Using Type Aliases in Python: A Practical Guide (with Examples)
  • Python: Defining distinct types using NewType class
  • Using Optional Type in Python (explained with examples)
  • Python: How to Override Methods in Classes
  • Python: Define Generic Types for Lists of Nested Dictionaries
  • Python: Defining type for a list that can contain both numbers and strings
  • Using TypeGuard in Python (Python 3.10+)
  • Python: Using ‘NoReturn’ type with functions
  • Type Casting in Python: The Ultimate Guide (with Examples)
  • Python: Using type hints with class methods and properties
  • Python: Typing a function with default parameters
  • Python: Typing a function that can return multiple types