Threads = requests per second x average response time. Work the formula, then find out whether concurrency or gigabytes runs out first on your plan.
· 11 min read · Pricing and economics
How many proxy threads do I need? Every vendor help desk answers that question with a ceiling — 100 per proxy, 500 per plan, 10,000 on this tier — and none of them answer the question that was asked. A ceiling tells you what you are not allowed to exceed. It does not tell you what your job requires.
The number you need is arithmetic, and it takes one line:
threads = target requests per second x average response time in seconds
That is Little's Law, applied to a request pool. Everything else in this article is either how to get honest inputs for it, or what breaks when you get the answer wrong in either direction.
Kill this ambiguity first, because the SERP for this question is full of pages that never do.
A thread, in every proxy provider's usage, is one connection open through the gateway at one instant. It has nothing to do with operating system threads, and nothing to do with how many CPU cores you have. Your crawler can run 200 green threads, 200 coroutines or 200 OS threads and still hold exactly 8 connections open, because 192 of them are parsing, sleeping or waiting on a queue.
The three words you will see mean the same thing:
What counts against the limit is a socket in flight. A sticky session holds one for as long as you hold the session, whether or not it is transferring bytes, which is why sticky work hits a ceiling that rotating work of the same volume does not. That mechanism is in rotating and sticky session ports.
Little's Law states that the average number of items in a system equals the arrival rate multiplied by the average time each item spends in it. For a request pool, items are in-flight requests, arrival rate is your throughput target, and time in system is your response time.
Job one. You need 200,000 pages in an 8-hour window.
200,000 / (8 x 3600) = 6.94 requests per second
If your average response takes 1.8 seconds:
threads = 6.94 x 1.8 = 12.5 -> 13
Thirteen. Not 100, not 500.
Job two. Same 200,000 pages, same window, but the target is slower and the average response takes 6 seconds:
threads = 6.94 x 6 = 41.7 -> 42
The workload did not change. The response time tripled and the thread requirement tripled with it. This is the sensitivity nobody warns you about: your thread requirement is a property of the target, not of your crawler. A target that slows down under load will silently raise your requirement while you are running, which is exactly when you cannot afford to hit a ceiling.
Run the formula in the other direction to find your ceiling in useful units:
requests per hour = 3600 x threads / average response seconds
At 20 threads and a 3-second average, that is 24,000 requests per hour. If your plan tops out at 20 threads and you need 40,000 per hour, no amount of tuning gets you there. You need a higher limit or a faster target.
Two numbers, not one.
The mean sizes the pool. The p95 tells you whether the pool stays
sized. If your mean is 1.8 seconds and your p95 is 6 seconds, then during the
tail every slot is occupied for more than three times as long as the formula
assumed, and throughput collapses to a third while the tail lasts. Size on the
mean, then check that you can tolerate 3600 x threads / p95 as a floor.
Measure through the proxy, not against the origin directly. The path you are sizing includes the gateway, the exit and the return leg. A timing taken from your laptop to the target measures a route your crawler never takes.
Instrument three timestamps per request: dispatch, first byte, last byte. Mean and p95 of last-byte-minus-dispatch is what goes in the formula. Anything shorter understates your requirement.
This is the question a per-GB plan actually turns on, and it appears nowhere in the vendor help-desk pages that own this SERP. Threads cap your rate. Gigabytes cap your volume. Whichever runs out first is your real limit, and it is usually not the one you have been tuning.
Requests per hour is 3600 x threads / seconds. Multiply by 24 for a day, then
by average response size for bandwidth. Two response sizes below: 22 KB, the
HTML component of the median home page in the HTTP Archive's July 2025 crawl
(Web Almanac 2025, Page
Weight), and 250 KB for a
heavier document or a small API payload. Gigabytes are decimal here,
1 GB = 1,000,000 KB.
| Threads | Avg response | Requests/hour | Requests/day | GB/day at 22 KB | GB/day at 250 KB |
|---|---|---|---|---|---|
| 5 | 8.0 s | 2,250 | 54,000 | 1.2 | 13.5 |
| 10 | 1.0 s | 36,000 | 864,000 | 19.0 | 216.0 |
| 20 | 3.0 s | 24,000 | 576,000 | 12.7 | 144.0 |
| 50 | 3.0 s | 60,000 | 1,440,000 | 31.7 | 360.0 |
| 100 | 5.0 s | 72,000 | 1,728,000 | 38.0 | 432.0 |
Read the last two columns as a monthly bill and the table stops being abstract. At 20 threads, a 3-second average and 250 KB responses, a job that runs around the clock consumes roughly 4,320 GB a month. At 22 KB responses the identical concurrency consumes roughly 380 GB. Same threads. An 11x difference in spend, driven entirely by what you chose to download. What exactly lands inside those gigabytes — headers, TLS, redirects and failures included — is in how bandwidth is metered.
Which means the useful conclusion is the opposite of the one the SERP gives you. If your responses are heavy, gigabytes bind first and adding threads just spends your budget sooner. If your responses are light, threads bind first and your bandwidth budget sits idle. Work out which side you are on before you tune anything, and check the number against per-GB rates for the pool you are running on.
More concurrency past the binding constraint does not add throughput. It moves your failures around.
407 THREADS_EXHAUSTED from the gateway. The connection ceiling on your
plan was reached. Requests are rejected at the gateway, not at the target,
and they are rejected instantly — which makes retry storms cheap to trigger
and expensive to recover from. The error and the plan model are in
thread limits on your plan, and the wider list is under
407 THREADS_EXHAUSTED and other gateway errors.The retry logic makes all four worse. A naive "retry three times immediately"
policy converts one rejection into four connection attempts, each of which
occupies a slot. Use exponential backoff with jitter, cap total attempts, and
treat 407 differently from 429 — a 407 means back off globally, because
every worker is hitting the same ceiling at the same moment. A 429 means back
off for that host only.
Less obvious, and more expensive over a quarter.
The fix is not always more threads. If your average response is 6 seconds and your p95 is 40, you are sized for a target that is timing out. Cut the timeout before you raise the limit — a 30-second timeout on a target with a 2-second mean is a slot held hostage by a request that was never going to complete.
Concurrency is not only a throughput knob. On a rotating port, every simultaneous connection is a separate exit address, so 50 threads means the target sees up to 50 distinct addresses hitting it at once. That looks different from 50 sequential requests from 5 addresses, and on some targets it looks worse.
Two consequences worth planning around:
Which of the two you should be running, and what each does to how the target sees you, is the subject of session model and IP footprint.
Raising a provider's thread limit does nothing if your client is capped lower. These are the knobs that decide how many sockets you actually open.
| Tool | Setting | Notes |
|---|---|---|
| Scrapy | CONCURRENT_REQUESTS, CONCURRENT_REQUESTS_PER_DOMAIN |
The per-domain value is the effective cap on a single-target crawl. Raising the global one alone changes nothing. |
| aiohttp | TCPConnector(limit=..., limit_per_host=...) |
Defaults to 100 total and unlimited per host. Set both explicitly. |
| httpx | Limits(max_connections=..., max_keepalive_connections=...) |
Keepalive connections still occupy a slot at the gateway. |
| undici (Node) | new Agent({ connections }) |
Per-origin. Left unset it is unbounded, so your real concurrency is whatever your task loop allows. Set it explicitly. |
| Playwright | Browser contexts, and pages within a context | Each page holds its own connections. A context with 10 open pages is not 1 thread. |
| Selenium | Driver instances | One driver, one browser, many sockets. Size by driver count, not by request count. |
Two rules that follow from the table. First, a browser-driven worker consumes several connections per page, not one, so the formula's output is a floor for browser work rather than a target. Second, connection pooling and keepalive mean an idle-but-open connection still counts — set your pool sizes to the number you sized for, not to the library default.
If the formula says you need more than your plan allows, and you have already checked that gigabytes are not the binding constraint, ask for an increase. Bring three things:
Increases are handled per plan and may require KYC. Start at request a higher thread limit.
Multiply the requests per second you need by your average response time in seconds. A job needing 7 requests per second against a target averaging 1.8 seconds needs 13 concurrent connections. The same job against a 6-second target needs 42. There is no universal number, because the requirement is set by the target's response time, not by your crawler.
Your account reached its concurrent connection ceiling and the gateway rejected the request before it reached an exit. It is not a target block and not an authentication failure despite the 407 status. Reduce concurrency, add backoff that pauses all workers rather than one, and confirm that held sticky sessions are not consuming slots you assumed were free.
No. Threads control how many requests are in flight, not how fast any one of them completes. Past the point where the target starts queuing you, adding workers increases mean response time, which reduces throughput and raises your thread requirement at the same time. Plot success rate against concurrency and stop at the peak.
They use the same number of slots but hold them longer. A sticky session occupies a connection for the duration of its window even during idle periods between requests, while a rotating request releases its slot on completion. Ten long-lived sticky sessions can therefore exhaust a limit that would comfortably carry a far higher rotating request rate.
Bandwidth, if your responses are heavy. Calculate 3600 x threads / average response seconds x 24 x average response size and compare it against your
monthly gigabytes. If that number exceeds your budget, gigabytes are your real
limit and extra threads only spend the budget faster. If it is well under,
threads are the constraint and it is worth asking for more.
Measure your mean and p95 through the gateway. Run the formula. Compare the resulting daily gigabytes against what you are willing to spend. Only then decide whether the limit is the problem.
Talk to support about a limit increase at /contact, and check the per-GB rates before you raise the number.