Fix ProxyError: Cannot Connect to Proxy in Python Requests
ProxyError cannot connect to proxy in Python requests? Decode the urllib3 traceback to the real cause (refused, DNS, timeout, 407, env vars) and fix it fast.
A ProxyError: Cannot connect to proxy from Python requests tells you almost nothing on its own, because the useful part is buried three exceptions deep. The proxy might be down, but far more often the port is wrong, your IP isn't whitelisted, a firewall is eating packets, or an environment variable is sending traffic to a proxy you never configured. This guide shows how to read the traceback from the inside out, gives you a short script that names the failing layer, and walks through the fix for each root cause.
Everything below was reproduced on 2026-10-30 with requests 2.32.5 and urllib3 2.6.3 (plus a urllib3 1.26.20 virtualenv for the older wording), against SparkProxy's live gateway and a few deliberately broken endpoints. Error strings are pasted verbatim from those runs.
The exact error and what it hides
Here is the full message as most people first see it, from a script running urllib3 1.26.x:
requests.exceptions.ProxyError: HTTPSConnectionPool(host='www.sparkproxy.io', port=443): Max retries exceeded with url: /robots.txt (Caused by ProxyError('Cannot connect to proxy.', NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x000001775052EBA0>: Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')))
On urllib3 2.x the same failure reads slightly differently:
requests.exceptions.ProxyError: HTTPSConnectionPool(host='www.sparkproxy.io', port=443): Max retries exceeded with url: /robots.txt (Caused by ProxyError('Unable to connect to proxy', NewConnectionError("HTTPSConnection(host='127.0.0.1', port=9): Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it")))
So if you searched for "Cannot connect to proxy" and your log says "Unable to connect to proxy", you're looking at the same error from a newer urllib3. The phrase itself is a version fingerprint: 1.26 says Cannot connect to proxy. (with a trailing period), 2.x says Unable to connect to proxy. Run python -c "import urllib3; print(urllib3.__version__)" and you'll know which half of this guide's examples match your output. On PyPI today the newest releases are urllib3 2.8.0 (uploaded 2026-09-15) and requests 2.34.2 (2026-05-14), so most fresh installs are on the 2.x wording.
Neither phrase means "the proxy is down." It means one thing: urllib3 failed before it ever finished talking to the proxy. The reason sits in the innermost exception.
Read the traceback from the inside out
The message is four wrappers nested like dolls. Peel them in this order.
The outer pool tells you less than you think. HTTPSConnectionPool(host='www.sparkproxy.io', port=443) names the target for HTTPS URLs, because requests tunnels to it with CONNECT. For a plain http:// URL it names the proxy instead (our runs showed HTTPConnectionPool(host='127.0.0.1', port=9)). People waste time debugging the target host when the pool line is just echoing it.
"Max retries exceeded" usually means one attempt. requests mounts its adapter with Retry(total=0, connect=None, read=False, ...); we printed that default from requests 2.32.5. Zero retries, one try, then this message. Nothing was hammered.
The ProxyError wrapper says the failure happened before the proxy connection was established. In urllib3 2.x's connectionpool.py, an OSError, NewConnectionError, TimeoutError or SSLError gets wrapped as a ProxyError only when conn.proxy is set and not conn.has_connected_to_proxy (source). That's why the phrase shows up for DNS failures, refusals, timeouts and tunnel rejections alike.
The innermost exception is the diagnosis. Match it against this table:
| Innermost exception (verbatim fragment) | Layer that failed | Real cause | Proxy swap fixes it? |
|---|---|---|---|
| `NewConnectionError ... actively refused it` (`[Errno 111] Connection refused` on Linux) | TCP to proxy | Wrong port, wrong host, nothing listening | Only if you had the wrong endpoint |
| `NameResolutionError ... Failed to resolve` (2.x) or `[Errno 11001] getaddrinfo failed` (1.26) | DNS for the proxy host | Typo, VPN/corporate resolver, container DNS | No, fix the name or resolver |
| `ConnectTimeoutError ... timed out. (connect timeout=4)` | TCP to proxy | Firewall or egress filter dropping packets | No, open the port or change network |
| `OSError('Tunnel connection failed: 407 Proxy Authentication Required')` | Proxy auth | IP not whitelisted, wrong or unencoded credentials | No, fix auth |
| `SSLError ... WRONG_VERSION_NUMBER` + "Your proxy appears to only use HTTP" | TLS to proxy | `https://` in the proxy URL for an HTTP proxy | No, change the scheme |
| `ConnectionResetError` / `RemoteDisconnected` | After TCP connect | Protocol mismatch, proxy closed you | Sometimes |
One version difference matters for retry code. Under urllib3 1.26.20 a connect timeout to the proxy surfaced as requests.exceptions.ConnectTimeout. Under 2.6.3 the identical failure surfaced as requests.exceptions.ProxyError wrapping ConnectTimeoutError. If your retry loop catches ConnectTimeout only, it silently stopped retrying when you upgraded. Catch requests.exceptions.ConnectionError (both are subclasses of it) or inspect the inner reason.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
A short script that names the failing layer
Rather than parse strings, test each layer yourself, in the order a request crosses them: DNS, then TCP, then the proxy handshake, then the target. The cheapest check runs first and the script stops at the first failure.
import os, socket, sys, requests
HOST, PORT = "gateway.sparkproxy.io", 11000
AUTH = os.environ.get("SPARK_PROXY_AUTH", "") # "user:pass", or empty if your IP is whitelisted
PROXY = f"http://{AUTH + '@' if AUTH else ''}{HOST}:{PORT}"
try: ip = socket.getaddrinfo(HOST, PORT, type=socket.SOCK_STREAM)[0][4][0]; print("1 DNS ok", ip)
except socket.gaierror as e: sys.exit(f"1 DNS FAIL {e} -> hostname typo, VPN or resolver")
try: socket.create_connection((ip, PORT), timeout=5).close(); print("2 TCP ok")
except OSError as e: sys.exit(f"2 TCP FAIL {e!r} -> refused: wrong port | timeout: firewall")
s = requests.Session(); s.trust_env = False # ignore HTTP(S)_PROXY and OS proxy settings
try: r = s.get("https://www.sparkproxy.io/robots.txt", proxies={"https": PROXY}, timeout=(5, 20)); print("3 PROXY ok, target answered", r.status_code)
except requests.exceptions.ProxyError as e: sys.exit(f"3 PROXY FAIL {getattr(e.args[0], 'reason', e)} -> 407: credentials or whitelist")
except requests.RequestException as e: sys.exit(f"4 TARGET FAIL {e!r}")
print("env/OS proxies requests would also apply:", requests.utils.get_environ_proxies("https://www.sparkproxy.io"))
Three real runs from our test box, which is deliberately not whitelisted on the gateway:
1 DNS ok 65.109.70.130
2 TCP ok
3 PROXY FAIL ('Unable to connect to proxy', OSError('Tunnel connection failed: 407 Proxy Authentication Required')) -> 407: credentials or whitelist
1 DNS ok 65.109.70.130
2 TCP FAIL ConnectionRefusedError(10061, 'No connection could be made because the target machine actively refused it', None, 10061, None) -> refused: wrong port | timeout: firewall
1 DNS FAIL [Errno 11001] getaddrinfo failed -> hostname typo, VPN or resolver
The second run used port 11009 and the third a misspelled hostname. Notice what the first run proves: the proxy is up, reachable and answering. It just doesn't know who we are. That one line settles the "is the proxy down?" question before you open a support ticket, which is why it belongs in your CI or container entrypoint.
Line 3 sets trust_env = False on purpose so the test measures only the proxy you passed. The last line then prints what requests would have added from your environment. If that dictionary isn't empty, read the env-var section before anything else.
Refused: wrong host, port or proxy scheme
A refusal is instant. A machine answered at that address and sent back a TCP reset because nothing listens on that port. The proxy service isn't "down" in the sense of being overloaded; you're knocking on the wrong door.
SparkProxy's gateway is gateway.sparkproxy.io, with port 11000 for HTTP/HTTPS with random rotation, 11002 for Sticky Sessions (the same IP held for five minutes), and 13000 for SOCKS5. A port outside that set, or one copied from another provider's snippet, produces exactly the actively refused it / Connection refused string. For background on why HTTP and SOCKS endpoints live on different ports, see proxy ports explained.
The other common refusal-adjacent mistake is the scheme of the proxy URL itself. The https key in the proxies dict says which target URLs use the proxy. The scheme inside the value says how to talk to the proxy. Writing https:// there makes urllib3 attempt TLS to a plain HTTP proxy, and 2.x is helpful about it:
ProxyError('Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#https-proxy-error-http-proxy', SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1081)')))
The correct shape, with both keys pointing at an http:// proxy URL:
import requests
PROXY = "http://USERNAME:PASSWORD@gateway.sparkproxy.io:11000"
proxies = {"http": PROXY, "https": PROXY} # value scheme is http:// even for https targets
r = requests.get("https://www.sparkproxy.io/robots.txt", proxies=proxies, timeout=(5, 20))
print(r.status_code)
HTTPS traffic is still end-to-end encrypted here. The client sends CONNECT www.sparkproxy.io:443 in clear text to the proxy, then runs TLS through the tunnel; the CONNECT method explained covers that handshake.
NameResolutionError: the proxy hostname does not resolve
urllib3 added NameResolutionError in 2.0.0 (released 2023-04-26, per its changelog). Before that, DNS failures hid inside a generic NewConnectionError, which is why older Stack Overflow answers misread them as refusals. Our 2.x output:
ProxyError('Unable to connect to proxy', NameResolutionError("HTTPSConnection(host='gateway.sparkproxy.invalid', port=11000): Failed to resolve 'gateway.sparkproxy.invalid' ([Errno 11001] getaddrinfo failed)"))
On Linux the bracketed part is usually [Errno -2] Name or service not known (the name doesn't exist) or [Errno -3] Temporary failure in name resolution (the resolver itself is unreachable). The distinction matters. The first is a typo or a stale hostname. The second is your network: a container with no DNS, a VPN that grabbed the resolver, or a corporate DNS that refuses external names.
Check the name from the same machine, with the same resolver Python uses:
nslookup gateway.sparkproxy.io
python -c "import socket; print(socket.getaddrinfo('gateway.sparkproxy.io', 11000)[0][4])"
If nslookup against a public resolver works but Python doesn't, the machine's configured resolver is the problem, not the proxy. In Docker, check /etc/resolv.conf inside the container. Hard-coding the proxy's IP address works as a short-term test, but providers can move gateways, so keep the hostname in production config.
Connect timeout: something is dropping your packets
A timeout is slow by definition: the client sent SYN packets and nobody replied before the connect timeout expired. Our reproduction against a blackholed address:
ProxyError('Unable to connect to proxy', ConnectTimeoutError(<HTTPSConnection(host='10.255.255.1', port=11000) at 0x273b1f0b750>, 'Connection to 10.255.255.1 timed out. (connect timeout=4)'))
A refusal means you reached a machine. A timeout means you probably didn't. The usual culprits, cheapest to check first:
- Egress filtering. Many cloud security groups, corporate networks and university networks allow outbound 80 and 443 only. Proxy ports like 11000 never leave the building.
- A local firewall (Windows Defender Firewall rules,
ufw, an endpoint-security agent) blocking the Python executable or the port. - A wrong IP that routes nowhere, often from a hard-coded address that has since changed.
Test the port without Python in the way:
Test-NetConnection gateway.sparkproxy.io -Port 11000
nc -vz -w 5 gateway.sparkproxy.io 11000
On our box Test-NetConnection returned TcpTestSucceeded : True for port 11000. If yours says False while the same command to port 443 on another host succeeds, the port is blocked between you and the proxy, and no amount of retrying will change it. Ask the network owner to allow it, or use the HTTPS-only route. Raising the connect timeout from 5 to 30 seconds does not help a firewall drop; it only makes each failure take longer. How to size connect and read timeouts separately is covered in proxy timeouts and retry logic.
Tunnel 407: IP not whitelisted or bad credentials
This is the case that most often gets misreported as "the proxy is down", because the ProxyError text looks identical to the network failures above. The inner exception gives it away:
ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 407 Proxy Authentication Required'))
We got that from SparkProxy's gateway with no credentials, from a machine whose IP isn't on the account's whitelist. Port 11002 answered the same way. The proxy accepted the TCP connection and replied straight away. It refused to open the tunnel because it couldn't identify the client.
There's a trap here: the same mistake against a plain http:// target raises no exception at all. The proxy's 407 comes back as an ordinary response, r.status_code == 407, and code that never checks status codes carries on parsing an error page. Only HTTPS URLs turn the 407 into a ProxyError. If you see a ProxyError for HTTPS and odd empty results for HTTP, it's one auth problem, not two.
SparkProxy supports two auth methods, and the fix depends on which you use:
- IP whitelist. Add your machine's public egress IP in the dashboard. The usual breakage is that the IP changed: a home connection renewed its DHCP lease, a cloud instance was stopped and started without a static IP, a VPN was switched on, or the job moved from your laptop to a CI runner with its own egress. Check the IP the internet sees from the failing machine, not the private
10.xor192.168.xaddressipconfigshows. - Username and password. Put them in the proxy URL. Special characters break URL parsing unless they're percent-encoded, so encode them instead of hand-escaping:
from urllib.parse import quote
import requests
user, password = "USERNAME", "p@ss:word/1"
PROXY = f"http://{quote(user, safe='')}:{quote(password, safe='')}@gateway.sparkproxy.io:11000"
r = requests.get("https://www.sparkproxy.io/robots.txt",
proxies={"http": PROXY, "https": PROXY}, timeout=(5, 20))
print(r.status_code)
The challenge-and-response flow behind the 407 is explained in how proxy authentication works. Never put a 407 in a retry loop. The credentials won't fix themselves on attempt three.
The proxy you never configured: env vars and OS settings
Sometimes the inner error names a host and port you don't recognise. That's the clearest sign of a leak: requests found a proxy in your environment and used it. requests reads http_proxy, https_proxy, all_proxy and no_proxy (uppercase too), and gets them through urllib.request.getproxies(). On Windows, CPython's version of that function falls back to the registry key HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings when no env vars are set, but only if ProxyEnable is non-zero. We read both from the CPython 3.14 source on our box. So a stale system proxy from an old VPN client can break a script that has no proxy code at all.
Two precedence rules catch people out, and we confirmed both in requests 2.32.5:
- A missing key gets filled from the environment. Pass
proxies={"http": PROXY}and forgethttps, and anHTTPS_PROXYvariable silently supplies the HTTPS proxy. Our merged settings came back as{'https': 'http://127.0.0.1:9', 'http': 'http://gateway.sparkproxy.io:11000'}, and the HTTPS request died on port 9. - Environment proxies beat
session.proxies. WithHTTPS_PROXYset andsession.proxies = {"https": PROXY}, the effective proxy was the environment's. The requests docs carry a warning thatsession.proxiesvalues "will be overwritten by environmental proxies" (advanced usage, issue #2018). Per-requestproxies=arguments do win.
Find out what's leaking in, then shut it off:
import urllib.request
import requests
print("OS/env proxies:", urllib.request.getproxies())
print("for this URL:", requests.utils.get_environ_proxies("https://www.sparkproxy.io/"))
session = requests.Session()
session.trust_env = False # ignore env vars, the Windows registry and .netrc
PROXY = "http://USERNAME:PASSWORD@gateway.sparkproxy.io:11000"
r = session.get("https://www.sparkproxy.io/robots.txt",
proxies={"http": PROXY, "https": PROXY}, timeout=(5, 20))
print(r.status_code)
trust_env = False also disables .netrc credentials and REQUESTS_CA_BUNDLE, so if you rely on a custom CA bundle, pass verify= explicitly. The narrower alternative is NO_PROXY: when we set NO_PROXY=www.sparkproxy.io, requests dropped the environment proxy for that host and fell back to the session's own. Shell-level cleanup is unset HTTP_PROXY HTTPS_PROXY ALL_PROXY on Linux and macOS, or Remove-Item Env:HTTPS_PROXY in PowerShell.
Resets, SOCKS ports and other look-alikes
A few failures look like "cannot connect to proxy" but aren't wrapped the same way, which breaks code that matches on the ProxyError class.
An HTTP client talking to a SOCKS port. Pointing http://gateway.sparkproxy.io:13000 at the SOCKS5 port connected fine at TCP level, then the server hung up on a request it couldn't parse. For an HTTPS target that surfaced as ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')), with no ProxyError and no proxy host in the message.
SOCKS errors never say "proxy." With socks5:// URLs (requests needs pip install requests[socks], which pulls PySocks), failures come back as ConnectionError wrapping NewConnectionError, and the host shown is the target. Our unauthenticated attempt against port 13000 returned Failed to establish a new connection: 0x02: Connection not allowed by ruleset. That 0x02 is the SOCKS server's own reply code (RFC 1928 defines it as "connection not allowed by ruleset"). From a client that sent no credentials and isn't whitelisted, treat it like a 407: same fix as above. The reverse mismatch, socks5h:// aimed at HTTP port 11000, produced a ConnectTimeout naming the target site, which sends people off to debug a perfectly healthy website. The full SOCKS setup is in our SOCKS5 setup guide.
A reset after connecting. ConnectionResetError before the tunnel opens means the proxy, or a middlebox in front of it, closed the socket. Protocol mismatches are the first suspect. If it happens intermittently under load, look at your concurrency against your plan, and see proxy error codes explained for how resets differ from 502 and 504 responses once the tunnel is up.
For async code the same root causes apply, but aiohttp names them differently (ClientProxyConnectionError for an unreachable proxy). The aiohttp side is covered in using proxies with requests and aiohttp.
What changing proxies can and cannot fix
Swapping to a different proxy, or a different provider, is the reflex fix. It only works for a minority of these errors.
| Root cause | Does another proxy fix it? | What does |
|---|---|---|
| Wrong port or scheme | Only by accident | Correct `host:port` and `http://` scheme |
| DNS failure on proxy host | No | Fix typo or resolver; test with `nslookup` |
| Egress firewall drops port | No, any non-443 port is blocked the same way | Open the port, or tunnel over 443 |
| IP not whitelisted / bad creds | No | Update whitelist or encode credentials |
| Env var or registry proxy leak | No, your code isn't using the proxy you chose | `trust_env = False`, `NO_PROXY`, full `proxies` dict |
| Proxy endpoint genuinely offline | Yes | Fail over to another endpoint |
| Target blocks the exit IP (403, 429, CAPTCHA) | Often | Rotation, fewer requests per IP, see the target's own response |
The last row isn't a ProxyError at all. Once you see an HTTP status from the target, the proxy connection worked and you've moved on to a blocking problem.
When outbound ports are locked: the HTTPS-only route
If the timeout section ended with "port 11000 is blocked and IT won't open it", you can still route through SparkProxy by calling the Scraping API over standard HTTPS on 443, which most egress rules already allow. The API fetches the page from its own network, so your machine never opens a connection to a proxy port.
import requests
r = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={
"url": "https://www.sparkproxy.io/robots.txt",
"render_js": "false", # plain fetch: 1 credit (render_js defaults to true, 5 credits)
"proxy_type": "premium", # file-based premium pool
},
timeout=(5, 60),
)
print(r.status_code, r.text[:200])
Set render_js=false explicitly, because the API renders JavaScript by default and that costs 5 credits instead of 1. New accounts get 1,000 free credits. If you need the job metadata envelope (job_id, status_code, duration_ms, credits_used), add json_response=true and base64-decode the body field before searching it; the format parameter only converts content (html, md, json and so on) and does not produce that envelope.
For the proxy route itself, SparkProxy plans start from $75/month (USD, read from sparkproxy.io on 2026-10-09) with unlimited bandwidth, so retries and diagnostic requests don't add traffic charges. There's a 24-hour free trial with no credit card, which is enough time to run the diagnostic script from every machine that will use the proxy and whitelist each egress IP before production.
Frequently asked questions
FAQ
Rarely. It means urllib3 failed before completing the connection to the proxy, and the innermost exception says why. A refusal, DNS failure, firewall timeout or a 407 tunnel rejection all produce the same outer message, and a 407 proves the proxy is up.
It's the same error from a newer urllib3. Version 1.26.x writes Cannot connect to proxy. and 2.x writes Unable to connect to proxy, so the phrase tells you which urllib3 major version produced the traceback.
requests defaults to Retry(total=0), so the message usually describes a single failed attempt. It's urllib3's generic wrapper text, not evidence that your script retried or hammered the proxy.
It reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY, and on Windows falls back to the Internet Settings registry proxy when ProxyEnable is on. Environment proxies also override session.proxies. Set session.trust_env = False or pass a complete proxies dict on every request.
Add the failing machine's public egress IP to your proxy whitelist, and re-check it whenever the machine changes network, restarts without a static IP or moves to CI. If you use username and password instead, percent-encode them with urllib.parse.quote in the proxy URL.
No, not for a standard HTTP proxy. The dict key ("https") selects which target URLs use the proxy, while the scheme inside the value is how you talk to the proxy. Use http:// there, and HTTPS traffic still travels encrypted through the CONNECT tunnel.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
Java Proxy Settings: Fix JVM 407 and Ignored Proxies
Java proxy settings that work: which JVM properties HttpClient reads, why Basic auth gets 407 on HTTPS, and the startup flag that fixes it, with a check each.
Sticky Session IP Changing Mid-Flow? Causes and Fixes
Sticky session IP changing mid-login or checkout? Check leaks, session ID format, the 5-minute window, parallel connections and exit churn in order.
httpx proxies Argument Removed? Fix ProxyError and 407
The httpx proxies argument is gone in 0.28. Fix the TypeError, then ProxyError 407, SOCKS ImportError and RemoteProtocolError, with verbatim errors and code.
