ERR_TUNNEL_CONNECTION_FAILED means your client sent an HTTP CONNECT request to a proxy and never got a working tunnel back. The website was never contacted. In Chromium it is error -111, a deliberate catch-all: it covers a proxy you could not reach, a proxy that refused you, and a proxy that could not reach the target.
What ERR_TUNNEL_CONNECTION_FAILED actually means
When a client fetches an HTTPS URL through an HTTP proxy, it cannot just hand the request over: the request is inside TLS, and the proxy is not the party the certificate belongs to. So the client asks the proxy to become a dumb pipe. That request is the CONNECT method, and it is plain text:
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
Proxy-Authorization: Basic dXNlcjpwYXNz
HTTP/1.1 200 Connection established
Only after that 200 does the TLS handshake with the real site begin, tunnelled through the proxy. ERR_TUNNEL_CONNECTION_FAILED is what Chromium raises when that exchange does not end in a usable tunnel. The comment above the definition in Chromium's own error list says as much: a tunnel connection through the proxy could not be established.
The distinction that matters, and that almost nobody writes down, is between the neighbouring error codes:
| Chromium code | Number | What it means |
|---|---|---|
| ERR_PROXY_CONNECTION_FAILED | -130 | The proxy itself could not be reached — DNS failed, or the socket to it did not open. Chromium's source explicitly notes this does not include failures during the CONNECT method. |
| ERR_PROXY_AUTH_REQUESTED | -127 | The proxy asked for authentication in order to establish the tunnel. |
| ERR_TUNNEL_CONNECTION_FAILED | -111 | The proxy was reached, the CONNECT was attempted, and the tunnel did not come up. This is the general-purpose bucket. |
| ERR_PROXY_UNABLE_TO_CONNECT_TO_DESTINATION | -186 | The proxy worked but could not connect to the destination — the hostname did not resolve, or the origin refused. Chromium uses it so the proxy is not blamed and marked bad. |
If you see -111 rather than -130, the machine reached the proxy. That single fact rules out half of the advice you will find on the subject.
Why the usual advice does not apply to you
On 7 September 2026 we pulled Google's US first page for this error through a residential exit and read every result we could fetch. The ten organic results were two YouTube videos, four forum or Q&A threads, one vendor knowledge-base article, one Microsoft Learn answer and two how-to posts. Six of them returned readable HTML to a plain HTTP client; two served a bot shell instead, and two were videos.
Across those six pages: zero mention the CONNECT request that failed. Zero mention curl, Playwright, Puppeteer, Selenium, SOCKS or a headless browser. Exactly one mentions 407 at all, in a single aside. The advice is uniform and it is written for someone who does not know they have a proxy — disable it, flush DNS, reset the browser. Google's AI Overview on the same page frames it the same way: the browser cannot make a secure connection through a proxy or a VPN.
That is a fine answer for a laptop with a leftover VPN configuration. It is useless if the proxy is there on purpose. And Google knows those people exist: the related searches on that very page include ERR_TUNNEL_CONNECTION_FAILED Playwright. Search the same error with the word "playwright" attached and the result set changes completely — Stack Overflow, a Crawlee issue, a Playwright issue, the r/Playwright subreddit. Two audiences, one error string, and page one only serves the first. The rest of this guide is for the second.
The seven things that produce this error
| Cause | How you recognise it | Fix |
|---|---|---|
| Wrong host or port | curl says it failed to connect to the host before any CONNECT line appears | Check the gateway hostname and port. Rotating, sticky and SOCKS5 endpoints on the same account usually listen on different ports. |
| No credentials sent | The proxy answers the CONNECT with 407 | Supply user and password. In a browser launched from code the credentials often have to go somewhere other than the proxy URL — see below. |
| Malformed username | 407 again, with credentials that look correct | Most residential gateways encode session and country in the username. An unknown country code or a stray separator is rejected as bad auth, not as a bad parameter. |
| IP not allowlisted | 403 on the CONNECT, or the connection is closed without an answer | Add the machine's real egress IP. Inside Docker, a CI runner or Kubernetes that is the node's address, not your laptop's. |
| Destination port not permitted | Works for ordinary https URLs, fails for the same host on a non-standard port | Many proxies only allow CONNECT to 443 and 80. Confirm the port is open on the plan before blaming the code. |
| TLS interception in the middle | Only some sites fail; the error disappears when antivirus HTTPS scanning or the corporate agent is off | Exclude the browser from the scanner, or install its CA. This is the common cause on a managed Windows machine. |
| The proxy cannot reach the target | Every other host works through the same proxy; one host does not | Retry through a different exit. Chromium may report -186 here instead, and the proxy is not at fault. |
Diagnose it in ten seconds with curl
The browser hides the CONNECT exchange. curl prints it, which is why every diagnosis should start there rather than in the automation code:
curl -v -x http://USER:PASS@PROXY_HOST:PORT https://example.com
Read the verbose output for one specific line and stop:
| What curl prints | What it tells you |
|---|---|
| Failed to connect to PROXY_HOST port PORT (exit code 7) | Nothing is listening, or a firewall is dropping you. The proxy never saw a request. Chromium would call this -130, not -111. |
| CONNECT tunnel failed, response 407 | Authentication. Credentials missing, wrong, or a malformed username. |
| CONNECT tunnel failed, response 403 | The proxy knows you and is refusing anyway: IP not allowlisted, plan exhausted, or a forbidden destination port. |
| CONNECT tunnel failed, response 502 or 503 | The proxy accepted you and could not reach the target. Change exit, not credentials. |
| Recv failure or an SSL connect error (56, 35) | The tunnel opened and then broke — an interception layer, or an exit that died mid-request. |
If curl gets a 200 and the browser still fails, the proxy is not the problem and the difference is in how the browser was configured. If curl fails too, fix that first; nothing downstream will work. Our proxy tester runs the same check from a browser when you cannot get to a terminal, and the proxy not working checklist covers the cases that are not tunnel-specific.
Fix it in Chrome, Edge and Opera
For a browser you drive by hand, the error almost always comes from configuration you did not knowingly set. Work through it in this order:
- Look at the system proxy. Chromium browsers use the operating system's proxy settings, not their own. A VPN or a security tool that was uninstalled carelessly frequently leaves a dead proxy entry behind, and every HTTPS request then dies on a CONNECT to a port nothing is listening on.
- Turn off HTTPS scanning in your antivirus, temporarily. Products that inspect encrypted traffic terminate the tunnel and rebuild it. When their certificate handling breaks, this is the error you get — and it is why the fix in several vendor forums is to whitelist the browser executable.
- Test a second browser and a phone on the same network. If only one browser fails, it is that browser's configuration. If everything fails, it is the network or the machine.
- Capture the evidence. Chromium's built-in net-export log records the CONNECT and the proxy's real answer, which is the difference between guessing and knowing.
Only after all four should you consider resetting network settings or changing DNS. Those steps do sometimes work, but they work by accident, and they will not tell you what was wrong.
Fix it in Playwright, Puppeteer and Selenium
Here the cause is narrower, and it is usually the same one: Chromium will not take proxy credentials from a command-line proxy URL. Passing them there is silently ignored, the proxy answers 407, and the driver surfaces it as net::ERR_TUNNEL_CONNECTION_FAILED. Each library has its own place to put them.
In Playwright, the credentials are separate fields, not part of the server string:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://PROXY_HOST:PORT",
"username": "USER",
"password": "PASS",
})
page = browser.new_page()
page.goto("https://example.com", timeout=60000)
In Puppeteer, the proxy goes in the launch arguments and the credentials are answered per page:
const browser = await puppeteer.launch({
args: ['--proxy-server=http://PROXY_HOST:PORT'],
});
const page = await browser.newPage();
await page.authenticate({ username: 'USER', password: 'PASS' });
await page.goto('https://example.com');
Selenium has no equivalent hook in the plain driver, which is why authenticated proxies there are handled with a wrapper or a small extension; the mechanics are in rotating a proxy in Selenium, and the Puppeteer-specific patterns in using a proxy in Puppeteer.
Three more traps that produce this exact error in automation and nowhere else:
- Container egress. Code that works on your machine and fails in Docker or Kubernetes is usually an allowlist problem: the node's IP is not the one you registered. Switch that account to username and password authentication, or add the egress range.
- A per-context proxy overriding the browser one. Playwright lets you set a proxy on the browser and on each context. The context wins, and an incomplete context-level proxy quietly discards the working browser-level credentials.
- Free or scraped proxy lists. A large share of the questions about this error come from lists where most entries are dead by the time you use them. A dead entry cannot fail any other way — see the 407 guide for how to tell a dead proxy from a rejected one.
When the tunnel is fine and the target is the problem
Once the CONNECT returns 200, the proxy has done its job, and any failure after that belongs to the site. This is worth stating because the two get confused constantly: a tunnel error is never an anti-bot block. Anti-bot systems answer through a working tunnel, with a status code. If your error has changed from ERR_TUNNEL_CONNECTION_FAILED to a 403 or a 429, you have made progress, not gone backwards — from there, 403 Forbidden while scraping and 429 Too Many Requests pick up the thread.
Choosing the right network also removes whole categories of tunnel failure. Ports, session control and allowlisting differ between residential proxies from $0.80/GB, rotating proxies from $0.50/GB and SOCKS5 proxies, which route any TCP traffic rather than only web requests and therefore sidestep the CONNECT method entirely.
Or skip the tunnel entirely
Every failure in this article exists because you are operating the proxy layer yourself. That is the right choice when you need a real browser under your control. It is a poor one when all you actually wanted was the contents of a page.
A scraping API moves the tunnel, the retries, the exit selection and the fingerprint to the other side of an ordinary HTTPS request. Our web scraping API returns any page as Markdown, HTML or structured JSON from $0.0002 per page, $0.001 with JavaScript rendering, and charges only for successes — a failed tunnel is not something you pay for or debug. The same calls are available to coding agents through the MCP server, and every account gets $2 of API usage free each month, which is enough to find out whether the proxy layer was ever worth maintaining.
Sources & further reading
- Chromium network error list (net_error_list.h)
- MDN Web Docs: the HTTP CONNECT method
- RFC 9110, HTTP Semantics: CONNECT
- Playwright documentation: network and proxy configuration
- curl manual: the proxy option
- Crawlee issue 3369: ERR_TUNNEL_CONNECTION_FAILED with an HTTPS proxy
- Kinsta: how to fix ERR_TUNNEL_CONNECTION_FAILED in Chrome