Fixing Slow Proxy Speeds

How to tell latency, throughput and concurrency problems apart with curl, measure each against a direct baseline, and match the result to a fix.

Start by checking whether individual requests are slow or the whole job is taking too long. Those are different problems with different fixes: high latency (each request takes a long time to return), low throughput (bytes arrive slowly), and a concurrency ceiling (each request is fine, but too few run at once). Moving to a closer exit does not help a job limited by a serial loop; adding workers does not help a single large download. The measurements below take a few minutes and tell you which one you have.

What counts as a normal result depends on where you, the exit and the target are, so this guide does not quote expected timings. It shows you how to produce your own baseline.

Measure latency: where the time goes

curl can break one 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/

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 working, which no proxy change can recover.

Two rules make the measurement useful. Run it five or ten times and take the median; a single sample can be distorted by a cold cache or a routing hiccup. Then run the identical command without -x for a direct baseline. The difference between the two medians is the cost of the detour through the exit, which is the number you are debugging; the target's own slowness travels with you either way.

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

Measure throughput

Throughput problems hide from latency tests, because a connection can open quickly and then trickle. Pull a large static file and let curl report the sustained rate. On a plan metered by the gigabyte each run counts against your allowance, so a 10 MB file is plenty:

curl -o /dev/null -s \
  -x http://USERNAME:PASSWORD@HOST:PORT \
  -w 'downloaded %{size_download} bytes at %{speed_download} bytes/sec\n' \
  https://proof.ovh.net/files/10Mb.dat

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 hold a static product with several, and if the collapse is consistent, open a ticket with both sets of numbers and a timestamp. Two medians and a time make the report actionable.

If your requests are API calls or HTML pages measured in kilobytes, throughput is rarely the problem. Your total time is latency multiplied by request count, which the next two sections address.

Check whether requests run one at a time

If your code issues requests sequentially, total runtime is request count multiplied by per-request time, and every millisecond of proxy latency is multiplied by the full count. Ten thousand requests at half a second each is over eighty minutes however fast the proxy is.

The check takes a minute: time one request, multiply by your request count, and compare with your actual runtime. If the multiplication predicts reality, the job is serial, and the fix is concurrency in the client rather than a change of proxy. You can see the difference from a shell before changing any 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 much faster than one. When they do, the fix is an async client, a worker pool, or a session per worker.

Check what is capping concurrency

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, and the per-credential connection number shown in the dashboard is advisory. The one enforced ceiling is a very high per-source-address connection cap on each node, there to stop a single client exhausting a server's file descriptors; ordinary workloads do not approach it. The exception is Rotating Unmetered, which is priced by concurrent connections and refuses connections above the plan's count while that many are open.

So when adding workers stops adding throughput, look at these, in the order they usually bite:

  • The HTTP client's connection pool. Most libraries default to a small per-host pool; ten workers sharing a pool sized for two means eight waiting in a queue you did not know existed. Raise the pool size to match the worker count and re-run the batch.
  • A single-threaded runtime. Python threads contend on the GIL for the parsing you do after a response arrives, and an async client whose event loop is blocked on CPU work stops issuing requests.
  • The target throttling you. Concurrency that plateaus against one host and not another is the host responding, not the network.

Does the exit's location affect speed?

Yes, on both legs. A proxied request travels from you to the exit to the target, and each leg adds round trips. For many small requests against a known target, put the exit near the target: the you-to-exit leg is paid once per connection, while the exit-to-target leg is paid on every round trip. For 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 choose the exit country in the username; country and city targeting has the syntax.

Why the first request through a proxy is the slowest

A new proxied HTTPS request pays a setup cost before any payload moves: a TCP handshake to the proxy, the CONNECT exchange, then a TLS handshake with the target, several round trips end to end. A reused connection pays none of that, which makes connection reuse the highest-leverage client-side change. In Python a requests.Session reuses connections; in Node an agent with keepAlive: true does the same. Browsers handle it for you.

Rotation interacts with this. On a rotating gateway an HTTPS tunnel keeps one exit for as long as it stays open, so a client that wants a fresh IP on every HTTPS request has to open a fresh tunnel each time and pays the setup cost each time; that is the price of per-request rotation, not a fault. If the workload does not need a new IP per request, keep connections alive and let each tunnel carry many requests; sticky vs rotating sessions covers when each mode fits, and the rotating proxies page describes the gateway.

How to tell whether the target is the slow part

Three signs point away from the proxy. 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. Or the direct baseline is as slow as the proxied run, meaning the target or its CDN is the bottleneck with or without you.

Progressive throttling is the target reacting to your traffic pattern; the response to that is in why your IP gets blocked.

Worksheet

Run these in order and write the numbers down.

  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 while watching the client's pool size.
  6. A second, unrelated target through the same proxy, to separate target-side trouble from path-side.

Then match the result to the fix: a large latency gap on every target points at exit location and connection reuse; a throughput collapse points at a ticket with your numbers attached; a serial loop points at client concurrency; a plateau at the client's pool size points at the pool; a single misbehaving target points at throttling.