
Proxy concurrency limits control how many requests your system can keep active at the same time. They are not the same as thread count, request rate, proxy pool size, or total monthly traffic.
For technical teams, the practical goal is not to maximize concurrency. It is to find the highest stable level that produces more successful responses without causing timeouts, rate limits, connection resets, or unnecessary retries.
A safe configuration usually combines three controls:
Understanding how threads, connections, and requests per IP relate makes it easier to scale without overwhelming your own infrastructure, the proxy service, or the target website.
Proxy concurrency is the number of requests actively passing through a proxy system at one moment.
If 50 requests have been sent and your application is still waiting for their responses, your current concurrency is approximately 50. It does not matter whether those requests were launched by 50 operating system threads, 50 asynchronous tasks, or a smaller number of workers using connection pooling.
Several terms are often treated as interchangeable even though they measure different things.
| Term | What it measures | Why it matters |
|---|---|---|
| Threads | Application execution units | Threads can create work, but do not directly determine network concurrency |
| Async tasks | Scheduled units in an event loop | Many tasks can run without creating one operating system thread per request |
| Connections | Open TCP or TLS transport sessions | Connections consume client, proxy, and server resources |
| In-flight requests | Requests sent but not yet completed | This is the clearest measure of active concurrency |
| Requests per second | Completed or attempted requests over time | This measures throughput, not concurrency |
| Requests per IP | Traffic assigned to each proxy address | This affects per-IP load and target-side rate limits |
| Proxy pool size | Number of available IP addresses | A larger pool provides capacity but does not set a safe request rate |
A crawler can have 1,000 proxies but only 20 concurrent requests. Another system might have 20 proxies and attempt 500 concurrent requests. The second system has a smaller pool but much higher per-IP pressure.
A thread is a unit of execution inside an application. Traditional synchronous programs may assign one request to each worker thread, but that is only one programming model.
An asynchronous client can manage hundreds of requests with one event-loop thread. In that case, increasing the number of tasks does not necessarily increase the number of operating system threads.
This is why a setting such as workers=100 or threads=100 does not tell you the actual proxy concurrency. You must also inspect:
Our comparison of Requests, HTTPX, and AIOHTTP for scraping explains how different Python clients handle synchronous and asynchronous workloads.
An HTTP connection is the transport path between two endpoints. When a proxy is involved, your client first connects to the proxy, which then connects or tunnels traffic to the destination.
HTTP/1.1 supports persistent connections, so multiple requests can reuse an established connection instead of performing a new connection setup every time. Clients should still remain conservative about opening many parallel connections because each connection consumes resources, and excessive connection counts can contribute to congestion or be treated as abusive traffic.
HTTP/2 works differently. It can carry multiple concurrent streams through one connection. A server or intermediary can advertise a maximum number of active streams through SETTINGS_MAX_CONCURRENT_STREAMS, which means one HTTP/2 connection may support several simultaneous requests without opening a separate connection for each one.
The practical lesson is simple:
Ten concurrent requests do not always require ten connections, and ten open connections do not always mean ten requests are active.
Requests per IP describe how much traffic each proxy address carries.
Suppose a crawler runs 100 concurrent requests through 20 proxies. If work is evenly distributed, each proxy handles about five active requests:
Per-IP concurrency = Global concurrency ÷ Active proxy count
100 ÷ 20 = 5 concurrent requests per IP
This is only an average. Poor rotation can assign 20 requests to one proxy while leaving another proxy idle.
For large workloads, traffic distribution matters as much as total pool size. Bulk proxies for large-scale web scraping are useful because they let teams spread active work across a wider set of IPs instead of concentrating requests on a few endpoints.
Concurrency measures active work. Throughput measures completed work over time.
A rough planning formula is:
Expected throughput ≈ Concurrency ÷ Average response time
If average response time is two seconds and the system maintains 40 requests in flight:
40 ÷ 2 seconds = approximately 20 requests per second
This estimate assumes the workload remains stable. Real throughput will be lower when requests fail, connections wait in queues, retries occur, or response times vary.
Increasing concurrency can improve throughput until one part of the system becomes a bottleneck. After that point, adding more requests usually increases waiting time rather than completed output.
Common bottlenecks include:
The correct objective is therefore not maximum requests in flight. It is maximum successful throughput at an acceptable latency and error rate.
There is rarely one universal proxy concurrency limit. Your effective limit is usually the lowest capacity across several layers.
Your code may restrict active tasks through:
A semaphore set to 50 means no more than 50 protected operations can run concurrently, even if 5,000 jobs are waiting in the queue.
HTTP libraries maintain connection pools to avoid creating a new transport connection for every request.
A client may have separate settings for:
When tasks exceed the available connection pool, requests wait for a free connection. This waiting time is easy to misdiagnose as proxy latency.
A proxy provider may apply limits by:
These limits are provider-specific. Never assume that buying 1,000 IPs automatically permits 1,000 simultaneous connections.
Ask the provider whether the plan includes account-level concurrency restrictions, per-IP connection limits, idle timeouts, or fair-use conditions.
A destination website can apply controls based on:
A proxy network may technically support 500 simultaneous requests, while the target begins returning 429 responses when one IP sends more than a small number of requests in a short window.
The target’s tolerance is often the real limit.
The crawler itself can fail before either the proxy or the target reaches capacity.
Watch for:
Scaling proxy concurrency safely requires end-to-end measurement, not just proxy monitoring.
There is no safe universal number of requests per proxy IP. A practical starting point can be calculated from expected throughput and measured latency.
Suppose a crawler needs to complete 30 successful requests per second.
Run a small test and record average and percentile latency. Assume the average completed request takes 1.5 seconds.
Required concurrency =
Target requests per second × Average response time
30 × 1.5 = 45 concurrent requests
Add a modest buffer for natural latency variation, but do not double concurrency without evidence. A starting global limit of 50 may be reasonable for a controlled test.
If 25 proxies are active:
50 concurrent requests ÷ 25 proxies = 2 requests per IP
Set the per-IP cap at two and observe the result.
Track:
A production proxy system should be evaluated through these metrics rather than raw request count. The guide to monitoring proxy performance in production provides a broader monitoring framework.
The following numbers are starting examples, not provider or target guarantees.
| Workload | Global concurrency | Active proxies | Approximate per-IP concurrency |
|---|---|---|---|
| Small catalog check | 10 | 10 | 1 |
| Recurring SEO monitoring | 30 | 30 | 1 |
| Public directory crawl | 100 | 50 | 2 |
| Large distributed crawl | 500 | 250 | 2 |
| Browser-based QA | 20 browser tasks | 20 | 1 |
Browser automation usually needs lower concurrency than raw HTTP requests because each browser context consumes more CPU, memory, bandwidth, and connection resources.
A system using Playwright should therefore not copy concurrency settings directly from a lightweight AIOHTTP crawler.
The following AIOHTTP example applies one global semaphore and one semaphore per proxy.
import asyncio
from collections.abc import Sequence
import aiohttp
GLOBAL_CONCURRENCY = 50
PER_PROXY_CONCURRENCY = 2
REQUEST_TIMEOUT_SECONDS = 20
PROXIES = [
"http://PROXY_USER:PROXY_PASSWORD@PROXY_HOST_1:PROXY_PORT",
"http://PROXY_USER:PROXY_PASSWORD@PROXY_HOST_2:PROXY_PORT",
]
async def fetch(
session: aiohttp.ClientSession,
url: str,
proxy: str,
global_limit: asyncio.Semaphore,
proxy_limit: asyncio.Semaphore,
) -> tuple[str, int | None]:
async with global_limit, proxy_limit:
try:
async with session.get(proxy=proxy, url=url) as response:
await response.read()
return url, response.status
except (aiohttp.ClientError, asyncio.TimeoutError):
return url, None
async def crawl(urls: Sequence[str]) -> list[tuple[str, int | None]]:
global_limit = asyncio.Semaphore(GLOBAL_CONCURRENCY)
proxy_limits = {
proxy: asyncio.Semaphore(PER_PROXY_CONCURRENCY)
for proxy in PROXIES
}
timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_SECONDS)
connector = aiohttp.TCPConnector(
limit=GLOBAL_CONCURRENCY,
limit_per_host=GLOBAL_CONCURRENCY,
)
async with aiohttp.ClientSession(
connector=connector,
timeout=timeout,
) as session:
tasks = []
for index, url in enumerate(urls):
proxy = PROXIES[index % len(PROXIES)]
tasks.append(
fetch(
session=session,
url=url,
proxy=proxy,
global_limit=global_limit,
proxy_limit=proxy_limits[proxy],
)
)
return await asyncio.gather(*tasks)
if __name__ == "__main__":
sample_urls = [
"https://example.com/page-1",
"https://example.com/page-2",
]
results = asyncio.run(crawl(sample_urls))
print(results)
The global semaphore prevents the crawler from opening unlimited work. The per-proxy semaphore prevents one proxy from receiving more than two simultaneous requests.
Production systems should also add domain-specific limits, structured retries, response validation, and centralized metrics.
Use incremental load testing instead of choosing a large number immediately.
For example:
| Test | Concurrency | Successful requests/sec | Error rate | P95 latency |
|---|---|---|---|---|
| A | 25 | 15 | 1% | 1.8 sec |
| B | 50 | 28 | 2% | 2.1 sec |
| C | 75 | 31 | 8% | 4.7 sec |
| D | 100 | 30 | 15% | 8.4 sec |
Test B is likely the better operating point. Test C produces only a small throughput improvement but sharply increases failures and latency.
When failures appear, do not automatically add more retries. Poor retry controls can multiply the original load. Use status-aware backoff and retry budgets such as those described in retry strategies for 429, 403, and 5xx responses.
A spike in 429 responses often indicates that the target is receiving requests faster than it accepts them. Lower per-domain or per-IP concurrency before expanding retries.
When concurrency increases by 50 percent but successful throughput barely moves, requests are probably waiting on a bottleneck.
Excessive open connections can exhaust resources in the client, proxy, or destination. HTTP specifications explicitly caution clients to limit simultaneous connections and note that excessive connection behavior may be considered abusive.
Round-robin assignment does not always produce balanced active load. Slow requests can keep some proxies occupied longer, while fast proxies receive more new work.
Track active requests and completed requests per proxy, not only assignment count.
If each original request produces two or three retry attempts, apparent concurrency may understate the actual pressure placed on the proxy pool and target.
One thread per proxy is not a reliable design rule. A proxy might safely handle more than one active request, or the target may require less than one sustained request per IP.
Measure behavior instead of tying two unrelated settings together.
A 1,000-IP pool provides distribution capacity. It does not guarantee that the client, proxy provider, or destination can sustain 1,000 simultaneous requests.
Different targets have different response times and traffic policies. Maintain separate per-domain queues and concurrency settings.
Creating a fresh connection for every request adds setup overhead and can increase socket pressure. Configure connection pooling and keep-alive behavior deliberately.
This makes diagnosis difficult and can produce a request storm. Change one variable at a time.
When producers add jobs faster than workers complete them, queues grow indefinitely. Use bounded queues so the crawl slows down when downstream systems cannot keep up.
Teams managing hundreds or thousands of IPs should also organize health data and load distribution carefully. See the practical guide to managing large proxy lists for pool-level controls.
Before setting production concurrency, confirm:
Do not rely on “unlimited” without reading what the term covers. Unlimited bandwidth, unlimited requests, and unlimited simultaneous connections are different claims.
A good limit is the highest tested level that maintains stable success rates, acceptable latency, and responsible per-IP traffic. There is no universal number because target behavior, response time, proxy type, and application architecture differ.
The technical capacity may be higher than the safe target-facing rate. Start with a low per-IP limit, often one active request for sensitive workloads, and increase only after measuring results.
No. A thread can make several sequential connections, while an asynchronous event loop can manage many connections with one thread.
No. HTTP/2 multiplexes several streams over one connection, but endpoints can still limit active streams, flow control, and resource use.
Increase the proxy pool when per-IP pressure is already high. Increase concurrency when proxies are underused and the rest of the system has measured capacity.
Yes. Once a bottleneck is saturated, added concurrency creates longer queues, more timeouts, and additional retries without improving successful throughput.
Proxy concurrency limits should protect three things at once: your application, the proxy infrastructure, and the target website.
Start with measured latency and a clear throughput goal. Set global, domain-level, and per-IP limits. Increase them gradually while tracking successful responses, latency percentiles, retries, and error classifications.
For workloads that benefit from distributing controlled traffic across many HTTP/HTTPS IPs, compare the available bulk datacenter proxy plans. ProxiesThatWork offers per-IP plans with HTTP/HTTPS support and IP authentication, making it possible to expand pool capacity separately from application concurrency.
Jesse Lewis is a researcher and content contributor for ProxiesThatWork, covering compliance trends, data governance, and the evolving relationship between AI and proxy technologies. He focuses on helping businesses stay compliant while deploying efficient, scalable data-collection pipelines.