Proxy Not Working? How to Read the Error and Fix It
By the Proxy11 team. Updated .
A proxy request has two hops: you to the proxy, then the proxy to the site. Almost every proxy error tells you which hop broke, once you know what to look for. Below is each common error as curl and Python actually print it, what it means, and what to try.
How we produced these errors
We didn't copy these messages from documentation. We set up small test proxies on an Ubuntu 24.04 server, each one broken in a different way: one that needs a password, one that can't reach the site, one that is overloaded, one that never answers. Then we ran curl 8.5.0 and Python requests 2.31 against each. The output below is pasted as it came out, apart from shortening a few long Python lines and putting a placeholder in place of one of our own hostnames.
Other curl versions word things a little differently, and Windows builds of curl print different SSL messages. The exit codes are the same everywhere.
First, find the hop that failed
| What you see | Where it broke | Look at |
|---|---|---|
| curl exit 7, Connection refused | You to the proxy | Address, port, firewall, or the proxy is down |
| curl exit 28, timeout | You to the proxy, usually | Dead proxy, or it accepts connections and never answers |
| CONNECT tunnel failed, response 407 | The proxy said no to you | Username and password |
| CONNECT tunnel failed, response 502 or 503 | The proxy to the site | The site, its DNS, or a busy proxy |
| curl exit 35, wrong version number | Your settings | https:// in the proxy URL for a plain HTTP proxy |
| 403, 429 or a CAPTCHA page | Nothing. The site answered | The site is blocking that IP |
The last row matters more than it looks. If you got any page back from the site, even an error page, the proxy worked. Don't throw the proxy away for that, and don't spend time debugging it.
Connection refused (curl exit 7)
$ curl -sS -x http://127.0.0.1:18089 https://example.com -o /dev/null curl: (7) Failed to connect to 127.0.0.1 port 18089 after 0 ms: Couldn't connect to server
Nothing is listening on that address and port. Note the time: 0 ms. A refusal comes back instantly, which is how you tell it apart from a timeout. Either the proxy is gone (common with free proxies), the port is wrong, or a firewall on your side sends back a reset.
Python wraps the same thing in a long message. The useful part is at the very end:
requests.exceptions.ProxyError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by ProxyError('Unable to connect to proxy',
NewConnectionError('...: Failed to establish a new connection: [Errno 111] Connection refused')))
"Max retries exceeded" sounds like it tried many times. It didn't. requests makes one attempt unless you set up retries yourself, and prints that phrase anyway.
Timeouts (curl exit 28): two very different problems
We made two broken proxies here. The first address doesn't exist, so the connection never opens. The second accepts the connection and then never says anything back, which is how a lot of dying or overloaded free proxies behave.
$ curl -sS --connect-timeout 5 -x http://10.255.255.1:8080 https://example.com -o /dev/null curl: (28) Failed to connect to 10.255.255.1 port 8080 after 5074 ms: Timeout was reached $ curl -sS --max-time 5 -x http://127.0.0.1:18084 https://example.com -o /dev/null curl: (28) Connection timed out after 5002 milliseconds
Same exit code, but Python treats them differently, and this catches out a lot of scrapers:
# Address that never answers the connection requests.exceptions.ProxyError: ... ConnectTimeoutError(..., 'Connection to 10.255.255.1 timed out. (connect timeout=3)') # Proxy that accepts the connection, then goes silent requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='example.com', port=443): Read timed out. (read timeout=3)
The second one is a ReadTimeout, not a ProxyError, even though the proxy is the problem. If your retry code only catches ProxyError, a silent proxy slips past it and your job stops. Catch requests.exceptions.Timeout as well, or catch requests.RequestException and decide from there.
Always pass a timeout. requests has none by default, and a silent proxy will then hold your script forever. A tuple such as timeout=(5, 15) gives 5 seconds to connect and 15 to wait for data.
407 Proxy Authentication Required
The proxy wants a username and password and didn't get the right ones. What you see depends on whether the site is HTTPS or plain HTTP:
$ curl -sS -x http://127.0.0.1:18081 https://example.com -o /dev/null
curl: (56) CONNECT tunnel failed, response 407
$ curl -sS -x http://127.0.0.1:18081 http://example.com -o /dev/null -w "%{http_code}\n"
407
For HTTPS, curl fails with exit 56. For plain HTTP there is no error at all: curl exits with 0 and you get a 407 page. Python does the same thing. For an HTTPS URL it raises ProxyError with Tunnel connection failed: 407 Proxy Authentication Required, and for an HTTP URL it returns a normal response with status_code 407. If your code doesn't check status codes, it will save that 407 page as if it were the real one.
The fix is to put the credentials in the proxy URL:
curl -x http://USER:[email protected]:8080 https://example.com proxies = { "http": "http://USER:[email protected]:8080", "https": "http://USER:[email protected]:8080", }
If the password contains @, :, / or #, encode those characters first, or the URL breaks:
$ curl -sS -x "http://demo:p@[email protected]:18081" https://example.com curl: (5) Unsupported proxy syntax in 'http://demo:p@[email protected]:18081': Bad hostname
Write @ as %40 and : as %3A. In Python, urllib.parse.quote(password, safe="") does it for you. Some providers also allow access by IP instead of a password. In that case a 407 usually means your IP changed and needs adding again.
502 Bad Gateway and 503 Service Unavailable
$ curl -sS -x http://127.0.0.1:18082 https://nosuchhost.invalid -o /dev/null curl: (56) CONNECT tunnel failed, response 502 $ curl -sS -x http://127.0.0.1:18083 https://example.com -o /dev/null curl: (56) CONNECT tunnel failed, response 503
Both come from the proxy, not from the site. A 502 means the proxy accepted your request but couldn't reach the site. We got this one by asking for a hostname that doesn't exist, and a site that is down or blocks the proxy's IP gives the same result. A 503 means the proxy is refusing work, usually because it's overloaded or being shut down.
To tell them apart, request the same URL without the proxy. If that fails too, the problem is the site. If it works, try a second proxy. A single proxy returning 502 for every site is broken. Drop it and move on.
Connection reset by peer
$ curl -sS -x http://127.0.0.1:18085 https://example.com -o /dev/null curl: (56) Recv failure: Connection reset by peer
Python shows it as ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer')). The proxy accepted the connection and then dropped it without answering. Free proxies do this when they're full, when they only allow certain clients, or when they're about to go offline. It's also what you see when you send HTTP to a SOCKS proxy, which is covered below.
Wrong proxy type in the URL
The scheme at the start of the proxy URL tells the client how to talk to the proxy. It has nothing to do with the site you are visiting, which makes it an easy one to get wrong.
# https:// in the proxy URL, but the proxy only speaks plain HTTP $ curl -sS -x https://proxy.example:80 https://example.com -o /dev/null curl: (35) OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version number # http:// used for a SOCKS5 proxy $ curl -sS -x http://127.0.0.1:18090 https://example.com -o /dev/null curl: (56) Recv failure: Connection reset by peer # socks5:// used for an HTTP proxy $ curl -sS --max-time 8 -x socks5://127.0.0.1:18082 https://example.com -o /dev/null curl: (28) Connection timed out after 8002 milliseconds
"Wrong version number" means your client started an encrypted connection and the other side replied in plain text. Almost every proxy on a public list is a plain HTTP proxy, so its URL should start with http://, even when you're visiting an https:// site. The site's encryption still happens inside the tunnel. urllib3 2.x, which requests uses underneath, notices this mistake and says so:
ProxyError('Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS,
try changing your proxy URL to be HTTP. ...', SSLError(... '[SSL: WRONG_VERSION_NUMBER] wrong version number'))
Mixing up HTTP and SOCKS gives less helpful errors, a reset in one direction and a silent hang in the other. If a proxy list says SOCKS5, use socks5:// or socks5h://. In Python that also needs an extra package, otherwise you get requests.exceptions.InvalidSchema: Missing dependencies for SOCKS support. Install it with:
pip install "requests[socks]"
Python: the request that skips your proxy
This one raises no error, which is why it's dangerous. requests picks the proxy by the scheme of the site's URL. If your dict only has an "http" key, every https:// request goes out directly, from your own IP:
proxies = {"http": "http://127.0.0.1:18082"}
requests.get("https://example.com", proxies=proxies) # status 200
# The proxy's log after that request:
''
We got a 200, and the proxy never saw the request. Always set both keys, usually to the same proxy URL. The value for "https" normally starts with http://, for the reason in the section above.
The opposite surprise comes from environment variables. requests reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY even when you don't pass proxies. With HTTPS_PROXY pointing at a dead proxy, our plain requests.get() with no proxy settings failed with the same Connection refused ProxyError as earlier. If your code gets proxy errors you didn't ask for, check env | grep -i proxy. To ignore those variables in one session, set session.trust_env = False.
Slow proxy? Find where the time goes
curl can split a request into stages. Run this through the proxy, then again without it:
curl -s -o /dev/null -x http://PROXY:PORT https://example.com \
-w "connect=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n"
Here's what we got from the same server. The second proxy was set to wait 2 seconds before contacting the site:
| Route | connect | tls | first_byte | total |
|---|---|---|---|---|
| No proxy | 0.050 | 0.589 | 0.610 | 0.610 |
| Normal proxy | 0.001 | 0.681 | 0.744 | 0.744 |
| Slow proxy | 0.001 | 2.511 | 2.595 | 2.595 |
Through a proxy, connect only measures the trip to the proxy. Here that was almost zero, because the test proxies ran on the same machine. Everything the proxy does after that, such as looking up the site, connecting to it and passing along the TLS handshake, shows up in the tls column. So if connect is fast and tls is slow, the delay is on the proxy's side or further on, not in your network.
If first_byte is much bigger than tls, the site itself is slow to answer, and changing proxy won't help much. With free proxies the tls column is usually the problem. Try a closer one, or filter for faster proxies before you start.
Works in curl, fails in the browser
curl and Python read proxy environment variables. Browsers mostly don't. Chrome and Edge use the operating system's proxy settings, and Firefox has its own settings under Network Settings unless you tell it to use the system's. So a proxy that works in your terminal may never reach the browser, or the browser may be using an older proxy you forgot about.
Other differences worth knowing:
- Chrome's
--proxy-serverflag takes no username or password. A proxy that needs a login either shows a login popup or fails, depending on how you started the browser. - Chrome reports
ERR_PROXY_CONNECTION_FAILEDwhen it can't reach the proxy at all, the same as curl exit 7.ERR_TUNNEL_CONNECTION_FAILEDmeans the proxy refused the CONNECT, like curl'sCONNECT tunnel failed. - A browser loads dozens of files from many hosts for one page. A slow free proxy that's fine for one curl request can make a page time out. Test with a single request before you blame the setup.
- Browser automation tools such as Playwright and Puppeteer take the proxy as a launch option, not from environment variables.
When the proxy works but your real IP still shows
Two leaks are worth checking, especially if privacy is the reason you use a proxy.
DNS. With socks5://, curl looks up the site's address itself and only sends the IP to the proxy. With socks5h://, the proxy does the lookup. We logged what our test SOCKS proxy received in each case:
$ curl -v -x socks5://127.0.0.1:18090 https://example.com * SOCKS5 connect to 172.66.147.243:443 (locally resolved) $ curl -v -x socks5h://127.0.0.1:18090 https://example.com * SOCKS5 connect to example.com:443 (remotely resolved) # What the SOCKS proxy logged for the two requests: client sent IPv4 address: 172.66.147.243:443 client sent hostname: example.com:443
With socks5://, your own DNS server sees every site you visit, even though the traffic goes through the proxy. Use socks5h:// in curl and in Python. In Firefox, turn on "Proxy DNS when using SOCKS v5". HTTP proxies don't have this problem, because the proxy always does the lookup for CONNECT requests.
WebRTC. Browsers can send WebRTC traffic, used for video calls, outside the proxy, and a page can use it to find your real IP. Open a WebRTC leak test page with the proxy on and compare the IPs it shows with your real one. In Firefox, setting media.peerconnection.enabled to false in about:config turns WebRTC off. Chrome can limit it with the WebRtcIPHandling policy, set to disable_non_proxied_udp.
Checklist
- Did any page come back from the site, even a 403? Then the proxy works and the site is blocking you.
- Instant failure means a wrong address or a dead proxy. A slow failure means it's silent or overloaded.
- The proxy URL starts with
http://for an HTTP proxy andsocks5h://for SOCKS, whatever the site's scheme. - Credentials are in the URL, with special characters encoded.
- In Python, set both
httpandhttpskeys, pass a timeout, and catchTimeoutas well asProxyError. - Check
env | grep -i proxyfor settings you forgot about. - Run the same request without the proxy to see whether the site itself is the problem.
If you're working through a list of free proxies, run them through our proxy checker first. It drops the dead ones before you spend time on any of the above.