Skip to content

Python requests ProxyError: Retries and Backoff for Proxies

Tell a wrong login from a dead connection in the error text, then wrap the call so only the failures worth repeating are repeated.

What ProxyError means and the 3 usual causes

ProxyError is a subclass of ConnectionError, so it is raised before the target site answers. The text at the end of the message tells you which case it is. In our local test a refused login ended with "Tunnel connection failed: 407 Proxy Authentication Required" and a closed port ended with "Connection refused".

Cause 1: wrong host, port or protocol. The Proxy302 gateway uses port 2222 for HTTP and 3333 for SOCKS5. A SOCKS5 address in requests also needs pip install "requests[socks]". Cause 2: the login is refused (407) because of a typo, a missing special character or a password with @ that was not percent-encoded in the URL. Cause 3: the asset has ended: a rented IP past its end date, or a traffic balance that is used up.

Not every failure is a ProxyError. A response with status 429 or 503 is a normal reply from the site or gateway, and a timeout is its own exception. They need different handling, as the table shows.

What to do with each failure

What to do with each failure
ItemRetry?What to do
ProxyError ending in 407NoFix the login and stop; every retry is refused again
ProxyError, Connection refusedNoCheck host, port, protocol and whether the asset has ended
ConnectTimeoutYes, a few timesThe Requests source says requests with this error are safe to retry
ReadTimeoutYes for GET, careful with POSTThe request may already have reached the site
429 Too Many RequestsYes, after waitingWait for the Retry-After time if it is sent, then slow down
502, 503, 504Yes, a few timesUsually temporary on the gateway or site side
403, 404NoThe answer will not change; change the request or the target

Based on the Requests source, the urllib3 Retry docs, MDN and our local test of 13 November 2026. The "yes" rows are advice for a small number of attempts, not a promise that a retry succeeds.

Fix and harden in 5 steps

  1. Step 1: Read the end of the error text

    Print str(exc) once. 407 means the login, Connection refused or a timeout means the address, port or protocol, or an ended asset.

  2. Step 2: Test the same proxy with curl

    Run curl -x http://LOGIN:PASSWORD@HOST:2222 https://ipinfo.io/json with the values from My assets. If curl works and Python does not, check the proxies dictionary and the environment proxy variables, which can override session.proxies.

  3. Step 3: Set a timeout on every call

    Without a timeout a request never gives up. Use a tuple such as timeout=(3.05, 20): connect and read are set separately.

  4. Step 4: Add retries with backoff

    For plain status codes mount a Retry adapter (first code block). To also handle ProxyError and the 407 stop rule, use the loop in the second code block.

  5. Step 5: Cap the attempts and log the last error

    Three or four attempts are enough for a temporary fault. After that, stop and record the last error text so you can see the real cause.

Retry adapter for status codes

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

PROXY = "http://LOGIN:PASSWORD@HOST:2222"
PROXIES = {"http": PROXY, "https": PROXY}

retries = Retry(
    total=3,
    backoff_factor=1,
    status_forcelist=[429, 502, 503, 504],
    allowed_methods={"GET"},
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retries))
session.mount("http://", HTTPAdapter(max_retries=retries))

response = session.get("https://ipinfo.io/json", proxies=PROXIES, timeout=(3.05, 20))
print(response.status_code, response.text)

Example only: replace LOGIN, PASSWORD and HOST with the values from My assets; percent-encode special characters such as @ in the login. The sleep between retries is backoff_factor * 2 ** previous retries.

A loop that stops on a wrong login

import random
import time

import requests

PROXY = "http://LOGIN:PASSWORD@HOST:2222"
PROXIES = {"http": PROXY, "https": PROXY}
RETRY_STATUS = {429, 502, 503, 504}
MAX_ATTEMPTS = 4


def fetch(url):
    last_error = None
    for attempt in range(1, MAX_ATTEMPTS + 1):
        delay = min(2 ** attempt, 30)  # 2, 4, 8, 16 seconds
        try:
            # a new Session opens a new connection, so a rotating line can give a new IP
            with requests.Session() as session:
                response = session.get(url, proxies=PROXIES, timeout=(3.05, 20))
            if response.status_code not in RETRY_STATUS:
                return response
            retry_after = response.headers.get("Retry-After", "")
            if retry_after.isdigit():
                delay = min(int(retry_after), 60)  # the server said how long to wait
            last_error = RuntimeError(f"HTTP {response.status_code}")
        except requests.exceptions.ProxyError as exc:
            if "407" in str(exc):
                raise  # wrong login: retrying will not help
            last_error = exc
        except (requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout) as exc:
            last_error = exc
        if attempt < MAX_ATTEMPTS:
            time.sleep(delay + random.uniform(0, 1))
    raise RuntimeError(f"gave up after {MAX_ATTEMPTS} attempts: {last_error}")


if __name__ == "__main__":
    print(fetch("https://ipinfo.io/json").text)

Example only, with placeholder login. Each attempt opens a new Session, and therefore a new connection; on a rotating line a new connection gets a new IP. The 407 check reads the error text, which our local test confirmed on requests 2.32.5.

Retries and rotation on Proxy302

On rotating residential every new connection gets a new IP, but a requests.Session keeps its connection alive, so many calls on one Session usually share one IP. That is why the loop above opens a fresh Session per attempt. Static ISP and datacenter IPs do not rotate during the lease, so a retry through them uses the same IP; they cost from US$0.25 (datacenter) and US$0.50 (static ISP) for 1 day.

Keep the number of attempts small. On per-GB lines every attempt that reaches the site uses traffic. There is no free trial; the smallest purchase is 1 GB of rotating residential traffic or one IP for one day.

FAQ

What does requests.exceptions.ProxyError mean?

A connection problem with the proxy: it is a subclass of ConnectionError. The end of the message says whether the login was refused (407) or the connection could not be opened.

Does requests retry by default?

No. The documentation says Requests does not retry failed connections by default. You add retries with urllib3.util.Retry through HTTPAdapter.

Does Retry retry POST requests?

Not by default. allowed_methods defaults to idempotent methods: DELETE, GET, HEAD, OPTIONS, PUT and TRACE. Add POST only if repeating it is safe.

How long does the backoff wait?

The wait grows by doubling with every retry, up to backoff_max (120 seconds by default). The urllib3 docs show 0.0, 0.2, 0.4 and 0.8 seconds for a factor of 0.1, so the first retry comes with no wait. The Retry-After header is honoured by default for 413, 429 and 503.

Why does ProxyError appear only sometimes?

An occasional connection failure is normal enough to retry two or three times. If it appears on every call, look for a fixed cause such as the login, the port, an ended asset or an environment proxy variable.

Reference links

6
  1. Requests source: requests.exceptions

    Official documentation · Confirmed on:

    Read on 13 November 2026: ProxyError subclasses ConnectionError; ConnectTimeout subclasses ConnectionError and Timeout and its docstring says requests that produced this error are safe to retry; ReadTimeout subclasses Timeout only; RetryError is "Custom retries logic failed".

  2. Requests docs: Advanced usage

    Official documentation · Confirmed on:

    Read on 13 November 2026: "By default, Requests does not retry failed connections"; retries use urllib3.util.Retry mounted through HTTPAdapter(max_retries=...); by default requests do not time out unless a timeout is set, and a tuple sets connect and read separately; a proxy URL can be written as http://user:password@host; session.proxies may be overridden by environment proxies; keep-alive is automatic within a session.

  3. urllib3 docs: urllib3.util.Retry

    Official documentation · Confirmed on:

    Read on 13 November 2026: backoff_factor is off by default (0); sleep is backoff_factor * (2 ** number of previous retries) up to backoff_max (default 120 s); status_forcelist is off by default; allowed_methods defaults to the idempotent methods DELETE, GET, HEAD, OPTIONS, PUT, TRACE; Retry-After is honoured by default for 413, 429 and 503; raise_on_status controls whether exhausted status retries raise.

  4. MDN: 429 Too Many Requests

    Official documentation · Confirmed on:

    Read on 13 November 2026: 429 means the client sent too many requests in a given time; a Retry-After header may say how long to wait before the next request.

  5. Local test of the code on this page

    Local example · Confirmed on:

    Ran on 13 November 2026 with requests 2.32.5 and urllib3 2.6.3 against a local test server acting as a proxy: a CONNECT refused with 407 raised ProxyError ending in "Tunnel connection failed: 407 Proxy Authentication Required"; a closed port raised ProxyError ending in "Connection refused"; a proxy that always answered 503 made Retry raise RetryError; the fetch() function on this page raised at once on the 407 and gave up after the set attempts on 503.

  6. Proxy302 product facts and price list

    Product observation · Confirmed on:

    Product facts file of the site (prices in US$, 1 PTC = US$1, protocols and ports, lease terms; protocol tests of 4 November 2026 and the rotating datacenter line of 10 November 2026). Checked on 13 November 2026.

Ready to try it?

Create an account, top up once, then pick rotating residential, static ISP or datacenter IPs and have one running in your browser within minutes.