A proxy pool can contain endpoints with very different capacity. Allowing fifty operations across the application does not stop forty-nine of them from choosing the same proxy.

If the policy is “at most four in-flight operations through each selected proxy,” express that policy directly. Keep a semaphore for each proxy identity, and acquire it around the work that consumes the proxy's capacity.

limits = {proxy: asyncio.Semaphore(4) for proxy in proxy_urls}

async def fetch(session, proxy, url):
    async with limits[proxy]:
        async with session.get(url, proxy=proxy) as response:
            return await response.read()

This sketch assumes asyncio has been imported and the proxy URLs have been validated. For public requests on a permitted public-proxy route, the HTTP proxy list supplies candidates for aiohttp's proxy URLs. A real application should also set its own request timeout and decide how much response data it needs. Reading a large file completely into memory is a separate resource decision.

Keep the permit for the expensive part

Releasing the semaphore as soon as the response headers arrive can defeat the intended limit. A streaming body may continue using the connection long afterward. Keep the permit until the body has been consumed or the response has been closed according to the application's workflow.

The nested context managers make cleanup visible when an exception or cancellation leaves the operation. Avoid acquiring manually and releasing only at the bottom of a successful function.

Connection limits answer another question

aiohttp's connector limits cap connections using the connector's documented rules. They remain useful alongside an application semaphore. A per-destination connection limit does not, by its name alone, express a budget shared by all destinations assigned to one proxy.

Also distinguish concurrency from request rate. Four long-lived requests and four very short requests use the same concurrency allowance while producing different requests-per-second rates. A service with a rate policy may require pacing or a separate rate limiter.

Choose a stable, non-secret proxy identifier for logs and metrics. If two credential-bearing URLs refer to the same capacity pool, decide whether they should share one budget. Conversely, different ports or accounts can represent separate limits when the operator documents them that way.

When throughput drops, inspect where work is waiting: your semaphore, the connector, proxy connection setup, or response reads. Raising every limit at once makes that distinction harder and can move the queue into a proxy you do not control.

Related articles