Tutorial29 September 20268 min read

Proxy timeout errors: find where the time goes

A proxy timeout can happen reaching the proxy, inside the tunnel, or waiting for the site. Tell which with one curl command, then set timeouts that fit.

A proxy timeout means one of three legs took too long: your machine reaching the proxy, the proxy reaching the website, or the website sending its answer. Each leg fails with a different error and has a different fix. The error text usually tells you which leg it was, and curl -w shows how long each one took.

This post is a diagnostic path. Start with the error you got, narrow it down with a timing command, and then set timeouts that fit the line you are on. Every snippet uses HOST, PORT, USERNAME and PASSWORD as placeholders for the values on your service's page in the dashboard.

Where can a proxy timeout happen?

A request through a proxy makes two connections. Your client connects to the proxy at HOST:PORT. For an https site, it then asks the proxy to open a CONNECT tunnel to the website, and talks TLS to the site through that tunnel. So there are three places time can go:

  1. You to the proxy. A TCP connection to HOST:PORT. It should take milliseconds to a few hundred milliseconds.
  2. The proxy to the site. The proxy resolves the site's name, connects out through the exit IP, and reports back whether the tunnel is open.
  3. The site's answer. The site thinks, then sends the page back through the exit.

A timeout on leg 1 is about your network, your settings or the proxy address. Leg 2 is about the target or the exit. Leg 3 is usually the target, sometimes a slow exit.

Step 1: read the error you already have

Match what you see to a leg before changing anything. These are real messages from curl 8, requests and httpx; wording shifts a little between versions.

Leg 1: your machine could not reach the proxy

  • curl: (7) Failed to connect to HOST port PORT ... Couldn't connect to server, ECONNREFUSED in Node, or NewConnectionError ... Connection refused inside a requests ProxyError. Something answered and said no. The host or port is wrong, or nothing is listening there for you. Copy both again from the service page and check the service still shows as active.
  • curl: (28) Failed to connect to HOST port PORT after 5001 ms: Timeout was reached, ConnectTimeoutError inside a ProxyError, or httpx.ConnectTimeout. Nothing answered at all. The usual cause is a firewall on your network (office, school, cloud security group) dropping outgoing connections to that port, or a host that is wrong in a way that points nowhere.
  • curl: (5) Could not resolve proxy: HOST, or NameResolutionError in requests. A typo in the proxy hostname, or your own DNS resolver is failing.

Leg 2: the proxy could not reach the site

  • curl: (56) CONNECT tunnel failed, response 502 (or 503), Tunnel connection failed: 502 Bad Gateway in requests, or httpx.ProxyError: 502 Bad Gateway. You reached the proxy and it accepted you, but it could not open a connection to the website. The site may be down, may refuse that exit, or the hostname may be wrong. Try the same credentials against https://api.ipify.org: if that works, the proxy is fine and the problem sits between the exit and your target.
  • CONNECT tunnel failed, response 407 is not a timeout. The proxy refused your login, and fixing a 407 walks through it.

Leg 3: connected, then no answer in time

  • curl: (28) Operation timed out after 30001 milliseconds with 0 bytes received, ReadTimeout in requests, httpx.ReadTimeout. The connection is up and nobody is talking. Either the site is slow to answer, the exit is slow, or the proxy is stuck on the tunnel.

Note that curl error 28 appears under two legs. "Failed to connect ... Timeout was reached" is leg 1, and "Operation timed out ... with 0 bytes received" is leg 3. The words after the number tell you which.

One trap in requests: when a proxy is set, a failure to connect to the proxy arrives as ProxyError, not as ConnectTimeout, even when it was a timeout. Read the "Caused by" part of the message, which carries the real reason.

Step 2: split the time with curl -w

curl can print a timestamp for each stage of a request. Run this once through the proxy:

curl -s -o /dev/null -x "http://USERNAME:PASSWORD@HOST:PORT" \
  -w 'dns        %{time_namelookup}s\nconnect    %{time_connect}s\ntunnel+tls %{time_appconnect}s\nfirst byte %{time_starttransfer}s\ntotal      %{time_total}s\nstatus     %{http_code}\n' \
  https://httpbin.org/ip

A healthy run looks something like this:

dns        0.000017s
connect    0.000109s
tunnel+tls 0.342913s
first byte 0.455784s
total      0.455814s
status     200

Each figure counts from the start of the request, so the gaps between them are what matter:

  • connect is when the TCP connection to the proxy finished. High here means leg 1: your network to the proxy.
  • tunnel+tls minus connect covers the proxy connecting to the site and the TLS handshake through the tunnel. High here means leg 2.
  • first byte minus tunnel+tls is the site working on your request and the first bytes coming back through the exit. High here means leg 3.
  • total minus first byte is the download. High here on a small page points to a slow exit; on a large page it is the page.

With a proxy set, dns is only the lookup of HOST. The proxy looks up the website's name itself, so your own DNS is never asked about the target.

Now swap the URL for your target and run it again. If the httpbin run is quick and your target's first byte gap is long, the site is slow to answer through any route, or slow for that exit in particular. Run the target once without -x to compare.

Is the proxy slow, or the target?

Repeat the measurement a few times. A loop makes the pattern easy to see:

for i in 1 2 3 4 5; do
  curl -s -o /dev/null -x "http://USERNAME:PASSWORD@HOST:PORT" \
    -w 'connect %{time_connect}  first byte %{time_starttransfer}  total %{time_total}  (%{http_code})\n' \
    https://httpbin.org/ip
done

On residential with Randomize IP, each run leaves from a different home connection. One slow line among quick ones is a slow exit, and a retry lands somewhere else. If every line is slow against your target but quick against httpbin, it is the target. ISP and datacenter IPs are static, so the same address serves every run: consistently slow on one site and fine on others can mean that site is throttling the address.

What timeout should I set?

Set two numbers, not one. A short connect timeout catches leg 1 fast, because a working proxy accepts a connection quickly. A longer read timeout gives slow exits and slow sites room. The timeout glossary entry gives the short version: allow more time on residential, and retry once before calling a request failed.

requests has no timeout at all unless you pass one, so a stuck read can hang a worker forever. Pass a (connect, read) tuple:

import time

import requests

PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
PROXIES = {"http": PROXY, "https": PROXY}


def probe(url):
    start = time.monotonic()
    try:
        r = requests.get(url, proxies=PROXIES, timeout=(5, 30))
        return f"{r.status_code} after {time.monotonic() - start:.1f}s"
    except requests.exceptions.ProxyError as error:
        return f"proxy leg failed: {error}"
    except requests.exceptions.ReadTimeout:
        return "connected, then no reply in time"
    except requests.exceptions.ConnectionError as error:
        return f"connection failed: {error}"


print(probe("https://api.ipify.org"))
print(probe("TARGET_URL"))

The read timeout is the longest gap between two chunks of data, not a cap on the whole download, so a page that trickles in can take longer than 30 seconds in total. Running the probe against an IP echo service and against your target side by side tells you which side is slow.

httpx goes the other way: it has a default, and the default is 5 seconds for each phase. That is tight for a residential exit fetching a heavy page. Raise the read side and keep connect short:

import httpx

PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
timeout = httpx.Timeout(30.0, connect=5.0)

with httpx.Client(proxy=PROXY, timeout=timeout) as client:
    r = client.get("https://api.ipify.org")
    print(r.text)

Concurrency and your own machine

Timeouts that appear only when the job runs at full speed, and vanish when you test one request by hand, usually come from your side. Too many requests in flight at once (concurrency) can exhaust your connection pool, your file descriptors or your uplink. In httpx that shows up as httpx.PoolTimeout, waiting for a free connection. Halve the number of workers and watch whether the timeout rate falls. If it does, the proxy was never the bottleneck.

High concurrency also presses harder on the target, and a site under pressure answers slower or stops answering, which looks like leg 3 from your side.

Retry, with limits

Timeouts are the textbook case for a retry with backoff. On residential with Randomize IP, a new attempt gets a new exit, which is often all it takes. Cap the attempts, though: a site that has stopped answering will not answer faster because you asked five more times. Retrying without burning bandwidth has the backoff code.

On our residential meter, connection failures (timeouts, resets, and errors from our own gateway such as a 407 or a 502) are billed at zero and still shown on your log, marked free. Anything the site sends back is traffic, a 429 or a 5xx page included, because inside an HTTPS tunnel our proxy cannot see the status code, only the bytes. The rules are under metering.

What to bring to Discord

If you have worked through the steps and it still looks like the proxy, ask in Discord with:

  • The time it happened, with a time zone. UTC is easiest.
  • The exit IP, if the request got far enough to have one. A request to https://api.ipify.org through the same credentials prints it.
  • The curl -v output, with the Proxy-Authorization line and the -x URL deleted. That line is your password in Base64.
  • The curl -w timings from above, through the proxy and without it.
  • Which service (residential, ISP or datacenter), the target hostname, and how many requests you run at once.

That is enough for someone to find the matching rows in the logs without a round of follow-up questions.

Quick answers

What does "ProxyError: Max retries exceeded" mean in requests? urllib3 gave up connecting through the proxy. The "Caused by" part says why: Connection refused and ConnectTimeoutError are leg 1, Tunnel connection failed: 502 is leg 2, and 407 is a login problem.

Why does the site load in my browser but time out through the proxy? Your browser goes direct, so it skips legs 1 and 2 entirely. Run the curl -w command with and without -x to see which leg adds the time.

Is "proxy connection refused" a timeout? No. Refused means something answered and said no, quickly. It almost always means a wrong host or port. A timeout means nothing answered.

What timeout should I start with? A 5-second connect timeout and a 30-second read timeout are reasonable starting points for residential. Datacenter and ISP exits are usually quicker, so you can tighten them once you have timings from your own target.

Got a follow-up question?

Ask it in Discord. The answer helps whoever reads the thread next.

Join the Discorddiscord.gg/proxypanda
Start with $5Ask in Discord