Fixing Slow Proxy Speeds

Most slow-proxy reports are really a concurrency ceiling or a serial loop. How to measure latency, throughput and parallelism before changing anything.

"Slow" is not a measurement. It is a feeling, and three very different problems produce it: high latency (each request takes a long time to come back), low throughput (bytes arrive at a trickle), and a concurrency ceiling (individual requests are fine, but the job as a whole crawls because too few run at once).

These have different causes and different fixes, and the fix for one does nothing for the others. Moving to a closer exit will not help a job that is bottlenecked on a serial loop. Adding worker threads will not help a single large download. Most frustrating hours spent on "the proxy is slow" are spent applying the right fix to the wrong problem. So this guide is deliberately front-loaded with measurement, and the diagnosis falls out of the numbers.

One honest note before the numbers: what counts as a normal result depends entirely on where you, the exit and the target each sit on the planet, so this guide will not quote expected timings. It will show you how to produce your own baseline, which is worth more.

Where is the time in a proxied request actually going?

curl can break a single request into named phases:

curl -o /dev/null -s \
  -x http://USERNAME:PASSWORD@HOST:PORT \
  -w 'dns %{time_namelookup}s  tcp %{time_connect}s  tls %{time_appconnect}s  first-byte %{time_starttransfer}s  total %{time_total}s\n' \
  https://example.com/

Reading the phases: tcp is when the connection to the proxy completed, tls is when the secure handshake with the target finished through the tunnel, and first-byte is when the target began answering. The gap between tls and first-byte is mostly the target server thinking: time no proxy change can recover.

Two rules make this measurement honest. First, run it five or ten times and look at the median; any single sample can be distorted by a cold cache or a routing hiccup. Second, run the identical command without -x for a direct baseline. The difference between the two medians is the true cost of the detour through the exit; that is the number you are debugging, as opposed to the target's own slowness, which travels with you either way.

For a repeatable harness that automates this comparison across products, see measuring proxy performance.

Measure throughput: a separate experiment

Throughput problems hide from latency tests, because a connection can open crisply and then trickle. Measure it by pulling a large static file and letting curl report the sustained rate:

curl -o /dev/null -s \
  -x http://USERNAME:PASSWORD@HOST:PORT \
  -w 'downloaded %{size_download} bytes at %{speed_download} bytes/sec\n' \
  https://speed.hetzner.de/10MB.bin

Any large file on a fast public server works. As before: several runs, a direct baseline, compare medians. If direct throughput is healthy and proxied throughput collapses, note the time of day (peak-hour congestion is real), try a second proxy from your plan if you have a static product with several, and, if the collapse is consistent and dramatic, open a ticket with both sets of numbers attached. Two medians and a timestamp make that report actionable; the word "slow" on its own does not.

Small-request workloads almost never have a throughput problem. If your requests are API calls or HTML pages measured in kilobytes, skip this section's fix entirely: your bottleneck is latency multiplied by request count, which brings us to the section that resolves most cases.

Is the proxy slow, or is my code serial?

Far more often than not, it is the code.

Here is the arithmetic that explains the majority of slow-scraper reports. If your code issues requests one at a time, total runtime is simply the request count multiplied by the per-request time, and every millisecond of proxy latency is multiplied by the full count. Ten thousand requests at half a second each is not a slow proxy; it is eighty-plus minutes of arranged, deliberate waiting.

The test takes one minute: time a single request, multiply by your request count, and compare to your actual runtime. If the multiplication predicts reality, your code is serial, and no proxy on earth fixes that. Concurrency does.

You can demonstrate the difference from a shell before touching your code:

seq 20 | xargs -P 10 -I{} \
  curl -o /dev/null -s \
  -x http://USERNAME:PASSWORD@HOST:PORT \
  -w '%{http_code} %{time_total}s\n' \
  https://example.com/

Ten workers should finish the batch dramatically faster than one worker would. When they do, the fix lives in your application (an async client, a worker pool, a session per worker), not in your plan or your endpoint.

Is there a limit on how many connections I can open at once?

This is worth stating precisely, because the intuitive answer is wrong. There is no per-account concurrent-connection limit enforced on the request path. The 10-thread figure quoted for the free tier is fair-use guidance rather than a gate; the per-credential connection number shown in the dashboard is advisory and nothing in the request path reads it. The one ceiling that is genuinely enforced is a very high per-source-address connection cap on each node, which exists to stop a single client exhausting a server's file descriptors. Ordinary workloads never approach it.

So when adding workers stops adding throughput, the cause is almost always on your side of the connection. The usual suspects, in the order they bite:

Measure before concluding: raise the client's pool size to match your worker count and re-run the same batch. If throughput moves, the ceiling was yours.

Does the exit's location affect speed?

Yes, and twice over, because both legs of the journey count.

A proxied request travels you → exit → target, and both legs contribute round trips. An exit far from both endpoints gives you the worst of both; an exit near one of them is a choice about which leg to shorten.

The useful heuristic: when the work is many small requests against a known target, put the exit near the target: the you→exit leg is paid once per connection, while the exit→target leg is paid on every round trip of every exchange. When the work is bulk transfer back to you, the exit-to-you leg carries the bytes and proximity to you matters more. Residential and rotating plans let you pick the exit country through the username; country and city targeting has the syntax.

Why is the first request through a proxy always the slowest?

Because a brand-new proxied HTTPS request pays a setup toll before any payload moves at all.

A brand-new proxied HTTPS request pays a toll before the first payload byte: a TCP handshake to the proxy, the CONNECT exchange, then a TLS handshake with the target: several round trips stacked end to end. A reused connection pays none of that.

This is why connection reuse is the highest-leverage client-side change available. In Python, a requests.Session reuses connections automatically where a bare requests.get loop may not. In Node, an agent with keepAlive: true does the same. Browsers handle it for you.

Rotation interacts with this directly. A gateway that rotates per request hands each request a different exit, so there is no connection to reuse; the toll is paid every time. That is the designed price of per-request rotation, not a malfunction. If your workload does not need a fresh IP on every request, sticky sessions restore reuse and reclaim the toll; sticky vs rotating sessions covers when each mode earns its cost, and the rotating proxies page describes the gateway itself.

How do I tell whether the target is the slow part?

Three signs point away from the proxy entirely. The slowness affects one target while others measure fine through the same endpoint. The slowness grows over a session (the first hundred requests brisk, the next hundred sluggish), which is the signature of progressive throttling, an anti-bot response rather than a network property. Or the direct baseline is just as slow as the proxied run, meaning the target (or its CDN's treatment of your region) is the bottleneck with or without you.

Progressive throttling in particular belongs to a different playbook: it is the target deciding it dislikes your traffic pattern, and the escalation path for that lives in why your IP gets blocked.

The worksheet

Run these in order and write the numbers down; the diagnosis is usually obvious by step four.

  1. Direct latency, five samples, median: the baseline.
  2. Proxied latency, same target, same count. The difference is the detour's cost.
  3. Direct and proxied throughput on a large file, if your workload moves bulk data.
  4. The serial check: does one request's time multiplied by request count predict your runtime?
  5. The parallel probe, stepping workers up toward your plan's thread cap.
  6. A second, unrelated target through the same proxy, to separate target-side trouble from path-side.

Then map result to fix: a large latency gap on every target points at exit geography and connection reuse; a throughput collapse points at a ticket with your numbers attached; a serial loop points at your own code; a plateau at the thread cap points at your plan; a single misbehaving target points at throttling.

The pattern to internalise is that measurement is cheap and guessing is expensive. Every fix on this page costs something (engineering time, a plan change, a different product), and thirty minutes with curl tells you which one you actually need before you spend anything.